DEV Community

John Pavlick
John Pavlick

Posted on

"You can't do nested record updates in Elm."

At least, not in an intuitive way. It's one of the first pain-points that a new developer runs into when getting started with the language.

Given:

type alias Model =
    { person : Person
    }

type alias Person =
    { firstName : String
    , lastName : String
    }
Enter fullscreen mode Exit fullscreen mode

... the natural thing to try when writing an update function looks like this:

update msg model =
    case msg of
        UpdatedFirstName firstName ->
            { model | person =
              { model.person |
                  firstName = firstName
              }
            }
Enter fullscreen mode Exit fullscreen mode

The compiler won't like it, and neither will you; this is not The Way.

The natural next step always looks something like this:

update msg model =
    case msg of
        UpdatedFirstName firstName ->
            let
                currentPerson = model.person

                updatedPerson =
                    { currentPerson | firstName = firstName }
            in
            { model | person = updatedPerson }
Enter fullscreen mode Exit fullscreen mode

But we're better. We can do better. Follow me.


Elm is a really, really simple language. Everything in the language follows a set of internally-consistent semantics; I find that when something does not work as I'd expect it to, it means that my expectations are wrong, which means that my mental model of the language is incomplete or wrong. (NB: people that claim to design systems that "work just like you'd expect them to" - how can they make that claim? Who is this subset of people for whom these things work exactly as they'd expect them to; and are they even the right subset of people to optimize for?1)

A few weeks ago, one of those Elm Lightbulbs fell neatly into its socket and I noticed one of Elm's guiding principles:

Functions go on the left. Parameters go on the right.

(Yes, I'm aware of |>; do you know how a mirror works?)

This seems obvious enough, but it has interesting implications, not all of which are immediately apparent. Everybody knows that if you have an a and you want to turn it into a b, all you have to do is put a function to the left of a that turns it into b - so you write a function, and call it:

aToB : a -> b

aToB valueA
Enter fullscreen mode Exit fullscreen mode

But what if you only need to use it once? Why not just write a lambda?

(\a -> b) valueA
Enter fullscreen mode Exit fullscreen mode

But you already knew that, and I still haven't addressed the elephant in the room. You probably knew, too, that you can write a lambda that takes a value and returns a record.

So why haven't I seen this before?

update msg model =
    case msg of
        UpdatedFirstName firstName ->
            { model |
                person =
                    (\p ->
                        { p | firstName = firstName }
                    ) model.person
            }
Enter fullscreen mode Exit fullscreen mode

"Wow, John. Neat party trick. But -"

You're right. This isn't a silver bullet. Sometimes, you should just write a function that does this for you. Some people use a builder pattern2, so they can do neat stuff like this:

update msg model =
    case msg of
        UpdatedFirstName firstName ->
            { model | person =
                model.person
                    |> withUpdatedFirstName firstName
            }

withUpdatedFirstName : String -> Person -> Person
withUpdatedFirstName firstName =
    (\person -> { person | firstName = firstName } )
Enter fullscreen mode Exit fullscreen mode

But you don't always need to reach for a builder pattern, when all you need to do is update a nested record once in your update function.

Besides, that's really not even what this little essay was about, in the first place:

As a beginner or intermediate Elm developer, if you get the sense that "there must be a better way to do this" - there probably is. Go look for it, and don't stop looking until you find it.

That's all. Good night, take care.


Update / edit: I posted this on the Elm Slack3 last night, and Jeroen Engels was kind enough to read though this, detail some errata, and suggest some improvements. I'm just going to quote him, here:

I think one of the options I end up going for in the end, is to have a separate function to update Person, in a separate Person module where the Person type is opaque. That's I think usually where you get the most benefits and it feels least cumbersome (if you already have the opaque type), and I think it would be valuable to mention it here.

Opaque types are beyond the scope of this post4, but Charlie Koster does a great job of explaining them, here, for anyone interested.


  1. Love or hate DHH, this document is still relevant: https://rubyonrails.org/doctrine - Ctrl+F "surprise" to get to the relevant section. 

  2. https://sporto.github.io/elm-patterns/basic/builder-pattern.html 

  3. http://elmlang.herokuapp.com/ 

  4. Because I don't feel qualified to explain them, and because my schtick is "saving beginners from themselves" and "atoning for my sins"; I still have much to learn. 

Top comments (3)

Collapse
 
brucou profile image
brucou

Is it not possible to have functional lenses in Elm? Having been using the language in the last two years so I wonder. Functional lenses (and functional optics in general) allow setting and updating any data structure and you can compose them with one another. I find lenses very elegant.

Collapse
 
jmpavlick profile image
John Pavlick

Austin Erlandson has done some good work here: package.elm-lang.org/packages/erla...

Collapse
 
mouvedia profile image
G.