I’m creating a type alias to a type record, but it’s not compiling (see attached non-compiling ellie). What I’m trying to do is take a Person from Model and ignore all fields other than name, while the compiler is telling me the name field doesn’t exist, as if it’s losing the name field somehow…
-- (elm-formatted code in linked ellie https://ellie-app.com/gpgnnY7mLYKa1)
type alias ExtensibleData c = { c | name : String }
type alias SubModel c = { subData : ExtensibleData c }
type alias Model = { subModel : SubModel Person }
subInit : ExtensibleData c -> SubModel c
subInit character = { subData = character }
type alias Person = { name : String , age : Int }
initialModel : Model
initialModel =
let
character : Person
character = Person "John Smith" 101
in
{ subModel = subInit character }
At first I thought it was similar to this old conversation, but that seems like the field was missing in the data passed in, whereas in my case, the field is present and not compiling.
Others have noticed in the past that type aliases to extensible records don’t seem to be fully supported:
and are generally considered best applied in practice when used (directly, it seems, not via alias) in function type signatures.
I applied this to your Ellie and it compiles: https://ellie-app.com/gpmHmb9bGBxa1
(I also changed your view function to confirm that it “works”.)
You can also see that the type alias is still being used, just not in the subInit signature anymore. So such type aliases are at least partly usable.
I don’t know if this should be considered a bug, or just a known limitation of how extensible records are intended to be used. But at least you can get the desired effect in your design if you avoid the alias.
Elm implementation of extensible records seems inconsistent and there is no public statement of whether it is a bug or it is intentional. Check this intellij-elm issue where this was discussed before:
With ExtensibleData c you idenity c as the type without the field name. So the subInit function return a type SubModel c which resolved to SubModel {age :Int}. This is not compatible with your Model where you expect SubModel { name : string, age: Int}
With ExtensibleData c you idenity c as the type without the field name . So the subInit function return a type SubModel c which resolved to SubModel {age :Int} . This is not compatible with your Model where you expect SubModel { name : string, age: Int}
I thought type alias ExtensibleData c = { c | name : String } means anything can be c, so long as it’s got name : String in it, but you’re saying this is cplusname : String.
That matches my understanding as well. I generally use extensible records as “this function takes any record, as long as it has a name field”, but leave it up the compiler to fill in “record without the name field”
I think what’s happening is mixing up the use of extensible records in a function definition and the use of extensible records in aliases.
The function
getName : { r | name : String } -> String
getName { name } = name
is an example of using extensible records to say “This function expects a record with at least the field name with type String.”
However
type alias WithName r = { r | name : String }
is saying “Add the field name with type String to the record r. Additionally, if r has an existing field name of any type, overwrite it.”
FWIW I personally avoid extensible records in aliases as I they feel, to me, like I’m trying to write OO style code. I do use them in my function type definitions quite frequently because they specify that the function needs to only operate on a subset of a record. I also don’t alias those because I find that it hides too much information. I.e. I find
getName : { r | name : String } -> String
a lot more clear than
getName : Named -> String
because I don’t know what Named is without using additional tools (IDE mouse hover or finding the definition of Named). For all I know, Named is a custom type, but it’s hard to know right away. I also only alias records when they get to 3 to 4 or more fields and only to save having to type out the record fields.
The problem comes indeed from this type annotation subInit : ExtensibleData c -> SubModel c
Both c’s in ExtensibleData c -> SubModel c need to be exactly the same type. That’s how generic types work.
Because { c | name : String } means that the c type doesn’t necessarily already have a name property then the compiler can’t assume it has one in SubModel c either.
Another way to make it compile is by using these types:
type alias SubModel c =
{ subData : c }
subInit : ExtensibleData c -> SubModel (ExtensibleData c)
Not that it’s any better to model it like this, but it illustrates the problem.