Contrasting F# and Elm’s record types

Hav­ing spent some time this week with Elm I have seen plen­ty of things to make me like it, a more in-depth review of my expe­ri­ence with Elm so far is in the works but for now I want to talk about Elm’s record type and how it com­pares with F# record type which us F# folks rely upon so often. At first glance, there are a lot of sim­i­lar­i­ties between the two, but upon clos­er inspec­tion you’ll find some notable dif­fer­ences.

 

F#

In F#, to cre­ate a record you first have to declare its type, and the idiomat­ic F# way is to use records as light­weight data con­tain­ers but you can option­al­ly add ‘mem­bers’ (i.e. prop­er­ties or meth­ods) to your record types too.

Whilst fields are immutable by default, they can be made muta­ble if you explic­it­ly mark them with the muta­ble key­word. Whilst this is not an encour­aged prac­tice the option is there for you if you need it, usu­al­ly as an opti­miza­tion to avoid the GC over­head of using the copy-and-update oper­a­tion (under the hood F#’s records are com­piled to class types so they’re heap allo­cat­ed and there­fore incurs allo­ca­tion and col­lec­tion cost), or because you need to interop with C#.

Advan­tages of using records over class­es include the abil­i­ty to use pat­tern match­ing and that they use struc­tur­al equal­i­ty seman­tic. Giv­en their role as light­weight data con­tain­ers, like tuples, is the sen­si­ble choice in most cas­es but you can still over­ride this behav­iour if you need to.

Whilst F# record types can imple­ment inter­faces, they can­not be inher­it­ed from, a fact that you can argue for or against. Per­son­al­ly I’m on the ‘argue for’ camp as it gives me future guar­an­tee of safe­ty and if I need to sup­port vari­ance I will intro­duce inter­faces and/or use com­po­si­tion instead.

 

Elm

In Elm, a record can exist on its own with­out you hav­ing to first define a type for it. Defin­ing a type mere­ly cre­ates an alias to help make your code more read­able and give you some sta­t­ic safe­ty where it’s need­ed.

Elm doesn’t have class­es but its records allow poly­mor­phic func­tions to be defined as part of the record. How­ev­er, these are not the same as F# record’s instance mem­bers as there is no this or self key­words in Elm (because Elm’s cre­ators con­sid­er it an extreme­ly bad prac­tice to mix data and log­ic, which I imag­ine most func­tion­al pro­gram­mers will agree).

Unsur­pris­ing­ly, Elm’s records can be pat­tern matched, but one caveat I found is that as far as I can tell there’s no way to cap­ture two records with the same field into two dif­fer­ent local vari­ables (see exam­ple below).

 

So far we have seen that Elm’s records are pret­ty sim­i­lar to their F# coun­ter­parts, where things get inter­est­ing is the exten­si­bil­i­ty options you have with Elm’s records.

Exten­si­ble Records

On top of the clone-and-update oper­a­tions (using the | label <- val­ue syn­tax) you can also:

  • add new fields using the = oper­a­tor, e.g. { x | species = “Jade Drag­on” } adds new species field with the val­ue “Jade Drag­on”
  • remove fields by using the minus oper­a­tor, e.g. { x – age } removes the age field from x when cloning

 

Com­posi­ble Record Types

Type alias­es defined using the { x | label : type } syn­tax (like Named and Aged in the above exam­ple) can be com­posed togeth­er using a some­what strange syn­tax, e.g. Name(Aged {}) which says that the record must con­tain all the fields defined in both Named and Aged. The inner most { } in this case rep­re­sents an emp­ty record, you can spec­i­fy bespoke labels and asso­ci­at­ed types there or use a type alias that is defined using the { label : type } syn­tax, like the Char­ac­ter type alias we defined in the above exam­ple.

 

Struc­tur­al Typ­ing

Final­ly, Elm’s records sup­port struc­tur­al typ­ing which allows func­tions to accept any record that has the required fields, this gives you the ben­e­fit of dynam­ic lan­guages.

In F#, whilst you don’t need to explic­it­ly spec­i­fy the type of the record when pat­tern match­ing (e.g. let show­Name { Name = name } = …), the type infer­ence process will still choose a type for you so you’re sta­t­i­cal­ly bound to a par­tic­u­lar type. You can, how­ev­er, sup­port struc­tur­al typ­ing in a sim­i­lar way using sta­t­i­cal­ly resolved type para­me­ters which also works on nor­mal class types but you lose the abil­i­ty to use pat­tern match­ing in the process, and I always find their syn­tax a lit­tle clum­sy so wher­ev­er pos­si­ble I would use inter­faces instead.

 

Related Readings

F# – Record types vs class­es

F# – Ref­er­en­tial equal­i­ty for Record types

F# per­for­mance test – structs vs Records

F# – sta­t­i­cal­ly resolved type para­me­ters

F# – XmlSe­ri­al­iz­er, Record types and [CLIMutable]

F# – Seri­al­iz­ing F# Record types

AOP – string intern­ing with Post­Sharp on F# record types

Elm – Exten­si­ble Records

Research paper – Exten­si­ble records with scoped labels