Updating nested records in Elm

Simple helper functions to pipeline nested record updates

More records | Photo credit
TL;DR: If you define your own record type (alias), Elm does not give you built-in functions to update a field in this record. You need to do this manually, or make your own setter function(s). For this, I always follow a simple pattern, that has made updating records easier and my code a lot cleaner.

The problem with updating nested records

Updating nested records in Elm can be a pain. Suppose you have a nested record structure like this:

type alias Movie = 
{ title : String
, director : String
, rating : Int
}

type alias Model =
{ currentMovie : Movie
, ...
}

At some point, you probably want to update e.g. the rating of the current movie. Elm does not allow you to that like this:

-- THIS WILL NOT COMPILE
case msg of
Rated newRating ->
{ model
| currentMovie =
{ model.currentMovie | rating = newRating }
}

The pattern { model.currentMovie | … } is unfortunately not allowed in Elm. So instead, you could write something like this:

case msg of
Rated newRating ->
let
oldCurrentMovie = model.currentMovie
newCurrentMovie =
{ oldCurrentMovie | rating = newRating }
in
{ model
| currentMovie = newCurrentMovie
}

It is readable, and easy to understand for others and my future self. Still, it’s a lot of code to update a field in a record. So I started looking for something better.

Recording help | Photo Credit

Special helper functions to the rescue

Whenever you define a record type alias in Elm, you get the dotted getter functions for free. Your setter functions however— to change something in your record (immutable-style of course)— you have to define for yourself.

For the example above, the more or less standard way is to define these setters as follows:

setTitle : String -> Movie -> Movie
setTitle newTitle movie =
{ movie | title = newTitle }

For each field, you would have to make a separate setter function.

This standard signature, with Movie -> Movie at the end, has a nice advantage: You can use pipes (|>) to update any selection of multiple fields.

newMovie =
oldMovie
|> setDirector "Clint Eastwood"
|> setRating 5

This bit of code will output a new movie, with the title and the director set to the new values.

But what about updating the nested currentMovie record inside the Model record? If you have made a setter function for your model, you could do this:

newModel =
oldModel
|> setCurrentMovie
( oldModel.currentMovie
|> setDirector "Clint EastWood"
)

Yuk. This doesn’t really looks like it’s worth making the custom setter functions.

In this case, the argument order of the setters gets in the way. To resolve that, you could define some additional setter helpers that simply flip the arguments. I know: this means you have to define even more custom functions. But bear with me.

asDirectorIn : Movie -> String -> Movie
asDirectorIn =
flip setDirector

My naming convention is "as"-your fieldName here-"In". By adding such an additional helper (which does one thing: it flips the arguments), you can create really compact updates like this:

newModel =
"Clint EastWood"
|> asDirectorIn oldModel.currentMovie
|> asCurrentMovieIn oldModel

You start with the new value of the field, and with each piped line, you add another wrapper around it, until you get to the outer/ upper record of the structure. With these new helper functions, you can update a field in a record structure that is several layers deep.

You can also combine them to update multiple fields at the lowest level:

newModel =
oldModel.currentMovie
|> setDirector "Clint EastWood"
|> setRating 5
|> asCurrentMovieIn oldModel

Only make the setters you need

I really like the readability and compactness of this pattern. Defining two custom setters for each field in a record is of course less enjoyable..

So it has become something of a habit for me to not define any setters up-front. But instead, I just throw in the set[FieldName] or as[FieldName]In functions as the need arises. The compiler has my back, and warns me that the function doesn’t exist (yet). And only then do I build the specific setter helpers. As it turns out, a lot of fields don’t need setters at all, so it saves me a lot of unnecessary work.

Hopefully these helpers can ease your pain of updating nesting records too.

If you just want to play with the pattern: you can tweak and run the demo below.


Pipes rock! 😉

Epilogue: For more advanced updates to deeply nested records, Evan did publish a Focus package at one point. Do make sure you read the caveats as well.

Show your support

Clapping shows how much you appreciated Wouter In t Velt’s story.