elm

Are extensible records useless in Elm 0.19?


Extensible records were one of the most amazing Elm's features, but since v0.16 adding and removing fields is no longer available. And this puts me in an awkward position.

Consider an example. I want to give a name to a random thing t, and extensible records provide me a perfect tool for this:

type alias Named t = { t | name: String }

„Okay,“ says the complier. Now I need a constructor, i.e. a function that equips a thing with specified name:

equip : String -> t -> Named t
equip name thing = { thing | name = name }  -- Oops! Type mismatch

Compilation fails, because { thing | name = ... } syntax assumes thing to be a record with name field, but type system can't assure this. In fact, with Named t I've tried to express something opposite: t should be a record type without its own name field, and the function adds this field to a record. Anyway, field addition is necessary to implement equip function.

So, it seems impossible to write equip in polymorphic manner, but it's probably not a such big deal. After all, any time I'm going to give a name to some concrete thing I can do this by hands. Much worse, inverse function extract : Named t -> t (which erases name of a named thing) requires field removal mechanism, and thus is not implementable too:

extract : Named t -> t
extract thing = thing  -- Error: No implicit upcast

It would be extremely important function, because I have tons of routines those accept old-fashioned unnamed things, and I need a way to use them for named things. Of course, massive refactoring of those functions is ineligible solution.

At last, after this long introduction, let me state my questions:

  1. Does modern Elm provides some substitute for old deprecated field addition/removal syntax?

  2. If not, is there some built-in function like equip and extract above? For every custom extensible record type I would like to have a polymorphic analyzer (a function that extracts its base part) and a polymorphic constructor (a function that combines base part with additive and produces the record).

  3. Negative answers for both (1) and (2) would force me to implement Named t in a more traditional way:

    type Named t = Named String t
    

    In this case, I can't catch the purpose of extensible records. Is there a positive use case, a scenario in which extensible records play critical role?


Solution

  • Type { t | name : String } means a record that has a name field. It does not extend the t type but, rather, extends the compiler’s knowledge about t itself.

    So in fact the type of equip is String -> { t | name : String } -> { t | name : String }.

    What is more, as you noticed, Elm no longer supports adding fields to records so even if the type system allowed what you want, you still could not do it. { thing | name = name } syntax only supports updating the records of type { t | name : String }.

    Similarly, there is no support for deleting fields from record.

    If you really need to have types from which you can add or remove fields you can use Dict. The other options are either writing the transformers manually, or creating and using a code generator (this was recommended solution for JSON decoding boilerplate for a while).

    And regarding the extensible records, Elm does not really support the “extensible” part much any more – the only remaining part is the { t | name : u } -> u projection so perhaps it should be called just scoped records. Elm docs itself acknowledge the extensibility is not very useful at the moment.