14

I am looking for a concise way of updating a nested value inside a record in Elm (0.18).

Given the following example:

person = { name = "Steven", address = { country = "Spain", city = "Barcelona" } }

I can update person.name to "Steve" using the following expression:

{ person | name = "Steve" }

However, I am looking for a way to update a nested value. For instance, I would like to update person.address.city to "Madrid". I tried the following:

{ person | address.city = "Madrid" } 
{ person | address = { address | city = "Madrid" } } 
{ person | address = { person.address | city = "Madrid" } } 

The compiler rejects all these variations. The shortest valid option I see is:

let personAddress = person.address in { person | address = { personAddress | city = "Madrid" } }

This seems to be a bit too much code just to update a nested value, Do you know if there is a better/shorter way of achieving that?

jakubr
  • 305
  • 1
  • 8
  • use elm-monocle https://github.com/toastal/toast.al-blog/blob/master/posts/code/2017-01-13-playing-with-prisms-for-the-not-so-isomorphic.md – rofrol Nov 28 '17 at 23:35

3 Answers3

16

Your last example with the let/in syntax is as concise as is possible in Elm 0.18 without resorting to additional packages.

That being said, in functional languages, you will often find the concept of Lenses useful for updating nested records. There is an Elm package at arturopala/elm-monocle which provide the ability to construct and execute lenses for more concisely getting and setting nested record values.

Using that package, you could build up lenses that would let you do concise things like this:

personWithUpdatedCity = personCity.set "Madrid" person

getCityOfPerson = personCity.get person

The downside is that you have to write all the lens wiring code yourself. In Haskell, this wiring can be done by the compiler. In Elm, we don't have that luxury.

The Elm code needed for the above lenses would be this:

addressCityLens : Lens Address String
addressCityLens =
    Lens .city (\cn a -> { a | city = cn })

personAddressLens : Lens Person Address
personAddressLens =
    Lens .address (\a p -> { p | address = a })

personCity : Lens Person String
personCity =
    compose personAddressLens addressCityLens

As you can see, it's tedious and much more code than you may expect to set a nested value. Due to that tedium, you may want to stick to the let/in example for the time being, unless your code uses nested sets all over the place.

There is an older discussion on the topic of making setting value easier in Elm here, but it hasn't been active for some time.

Chad Gilbert
  • 36,115
  • 4
  • 89
  • 97
  • Thank you for the detailed response. I hoped for some syntactic sugar from Elm for these kind of constructs. – jakubr Feb 14 '17 at 13:53
  • 2
    Today I published a post on how I make my own syntactic sugar to do nested record updates. https://medium.com/elm-shorts/updating-nested-records-in-elm-15d162e80480?source=collection_home---5------0---------- – wintvelt Feb 16 '17 at 13:13
  • That's pretty slick, @wintvelt! I like the readability and conciseness of the flip/piping method. – Chad Gilbert Feb 16 '17 at 13:21
9

If I need to do this sort of update a lot, I build a helper

updateAddress : ( Address -> Address ) -> Model -> Model 
updateAddress fn m = 
  {m | address = fn m.address }

and use it, e.g.

updateAddress (\a -> { a | city = Madrid }) model
at.
  • 50,922
  • 104
  • 292
  • 461
Simon H
  • 20,332
  • 14
  • 71
  • 128
0

You can make a function that receives a Person (which is a type alias for { name: String, address: { city: String, country: String }) and a Stringand returns the updated record, like this:

updateCityAdress : Person -> String -> Person
updateCityAddress person newCity =
    { name: person.name, address = { country: person.address.country, city = newCity }

> updateCityAddress person "Madrid"
> { name = "Steven", address = { country = "Spain", city = "Madrid" } }