Understanding extensible records

Hi there!

I’m trying to understand how to use extensible records to do what I want. Here’s a quick example of the situation I’m in:

module Foo exposing (..)


type alias Foo =
    { name : String
    }


type alias Selectable a =
    { a
        | isSelected : Bool
    }


isSelectableSelected : Selectable a -> Bool
isSelectableSelected selectable =
    selectable.isSelected


getFooName : Foo -> String
getFooName foo =
    foo.name


makeSelectableFoo : Selectable Foo
makeSelectableFoo =
    { name = "Bar"
    , isSelected = True
    }


getMyFooName =
    let
        selectableFoo =
            makeSelectableFoo
    in
    getFooName selectableFoo

This does not compile, with the following error:

-- TYPE MISMATCH --------------------------------------------------- src/Foo.elm

The 1st argument to `getFooName` is not what I expect:

37|     getFooName selectableFoo
                   ^^^^^^^^^^^^^
This `selectableFoo` value is a:

    Selectable Foo

But `getFooName` needs the 1st argument to be:

    { name : String }

Hint: Seems like a record field typo. Maybe isSelected should be name?

Hint: Can more type annotations be added? Type annotations always help me give
more specific messages, and I think they could help a lot in this case!

I’m trying to find a way to write getFooName in a way that allows me to use it with whichever extended type I choose (Selectable for instance). As far as this function is concerned, anything with name property should work out, because all it cares about is whether the argument has the structure of a Foo (which only has a name).

I understand I could make it work with making a Fooable type alias and having a bogus “base type”:

module Foo exposing (..)


type alias Bar =
    {}


type alias Fooable a =
    { a
        | name : String
    }

-- ....

getFooName : Fooable a -> String
getFooName foo =
    foo.name


makeSelectableFoo : Selectable (Fooable Bar)
makeSelectableFoo =
    { name = "Bar"
    , isSelected = True
    }


getMyFooName =
    let
        selectableFoo =
            makeSelectableFoo
    in
    getFooName selectableFoo

This gives me the behaviour I want, but I’ve had to create a bogus empty record type and “Fooable” doesn’t make as much sense as Foo.

How is this type of problem typically solved?

You don’t really need to define types, you could write getFooName as:

getFooName : { r | name : String } -> String
getFooName foo =
    foo.name

and it would work with any record with a name : String field.

But actually there is already a syntax for such functions in the language itself, check in elm repl:

> .name
<function> : { b | name : a } -> a

So .name is exactly that, and uses extensible records. You can use foo.name or .name foo.

> .name { name = "foo" }
"foo" : String

> { name = "foo" }.name
"foo" : String
1 Like

Thank you for your reply! I hadn’t thought about defining getFooName's argument as an extensible record itself. I’ll keep that in mind!

For my real use case however, the burden I was trying to avoid was redefining Foo's fields elsewhere - I should have been clearer about that in my original post.

In a situation where Foo has 10 fields, all of which are used in this hypothetical getFooName, say I’m writing 5 functions like getFooName, repeating these 10 fields definition 5 times. Now if I change or add a field to Foo, I have to make 5 changes. (While not as extreme, my real-world use case defines 3 fields, but is used in 9 places, some of which use it as Selectable, some of it not.)

On the other hand, in the example, if I added or changed a field in Selectable, there’s only 1 place I’d need to change a definition, since isSelectableSelected (and every other function I could have written that used Selectable) doesn’t redeclare those fields. I am trying to find a way to get the same level of practicality for a base type alias as with an extended record.

It doesn’t seem feasible this way, but I must admit I’m curious as to why the compiler doesn’t permit the passing of a “bigger record” to a function as long as it defines the required fields.

I’m not sure to understand, but I could have written:

getFooName : Fooable a -> String
getFooName foo =
    foo.name

It would have been the same, without repeating the fields.

You can also make a type alias Foo = Fooable {}, and then use Foo, and all your hypothetical functions could use a Fooable a without repeating the fields:

module Foo exposing (..)


type alias Fooable a =
    { a
        | name : String
        , age : Int
    }


type alias Foo =
    Fooable {}


type alias Selectable a =
    { a
        | isSelected : Bool
    }


isSelectableSelected : Selectable a -> Bool
isSelectableSelected selectable =
    selectable.isSelected


getFooName : Fooable a -> String
getFooName foo =
    foo.name ++ String.fromInt foo.age


makeSelectableFoo : Selectable Foo
makeSelectableFoo =
    { name = "Bar"
    , age = 42
    , isSelected = True
    }


getMyFooName =
    getFooName makeSelectableFoo

in elm repl:

> import Foo
> Foo.getMyFooName
"Bar42" : String

Note: again, in this meaningless example, isSelectableSelected = .isSelected. It could use more fields though in a real use-case.

That said, extensible records have their use, but rarely as a data modeling one. So beware, you may reach some dead-ends trying to do this. They are mostly useful for narrowing functions arguments (https://medium.com/@ckoster22/advanced-types-in-elm-extensible-records-67e9d804030d).

3 Likes

That is pretty nice and works well to avoid redefining the fields, thank you! The only downside that remains is the relation between Foo and Fooable. It won’t necessarily be intuitive for a newcomer to understand the difference between the two.

I suppose extensible records are not really made for this use case. For now on my real-life project, I’ve kept a record with isSelected defined in, without extensible records, as it’s much simpler. Only downside is, I have to sometimes provide bogus isSelected data just so I can pass it to some functions.

I’ll keep on coding and I might find a better way to solve the underlying problem :construction_worker_man:

1 Like

Yeah, Elm works usually better with flat models (without nested extensible records or nested fields).

Bogus data should be avoided. A simple way would be to use a Maybe Bool for the model selection status and for isSelected. It might be preferable though to use a custom type like:

type Selection
    = Unselectable
    | Unselected
    | Selected
3 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.