Concise way of updating a nested value inside a re

2019-02-06 17:51发布

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?

标签: record elm
3条回答
Viruses.
2楼-- · 2019-02-06 18:30

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
查看更多
【Aperson】
3楼-- · 2019-02-06 18:31

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.

查看更多
神经病院院长
4楼-- · 2019-02-06 18:31

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" } }
查看更多
登录 后发表回答