Extensible Records seems to be pruning fields and failing to compile

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 }

I’m sure I’m doing something wrong, but I’m not clear what.

Ellie failing to compile: https://ellie-app.com/gpgnnY7mLYKa1, and an attempt with a model with a type variable, in case that was it https://ellie-app.com/gpgqPPngzjNa1

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.

1 Like

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.

1 Like

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:

1 Like

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 c plus name : String.

1 Like

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.

6 Likes

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.

1 Like