at.
at.

Reputation: 52540

How to add a nested element in a model in Elm

Is there a standard way to do the following?

{ model | country =
  { model.country | state =
    { model.country.state | city =
      { model.country.state.city | people =
        model.country.state.city.people ++ [ newPerson ]
      }
    }
  }
}

Of course, country, state, and city are records nested in the model. I just want to add a person in the nested city record.

The above doesn't actually work. I get the following error on the first mention of model.country:

I am looking for one of the following things:

"'"
"|"
an equals sign '='
more letters in this name
whitespace

The way I was able to get this to work was to simply call a function at each step:

{ model | country = updateCountry newPerson model.country }

updateCountry person country =
  { country | state = updateState newPerson country.state }

And then the same for updateState and updateCity...

Upvotes: 2

Views: 572

Answers (3)

halfzebra
halfzebra

Reputation: 6797

As of today(0.18.0), arbitrary expressions are not allowed in record update syntax.

In other words, you can not access field of a record during the update:

model = { model.topProperty | childProperty = "Hello" }
          ^^^^^^^^^^^^^^^^^
            Syntax error

It is a planned feature for Elm Compiler, but for now, you should consider restructuring your model or using one of the verbose workarounds.

Personally, I prefer let..in expression for that, but I never use records with depth, higher than 3.

let..in example

It looks super-verbose, but there is nothing bad with this approach. You will be abe to refactor it when Elm Compiler will support a better syntax.

Use this as a starting point for developing a set of helper functions for updates of different levels.

let
    -- Deconstruct on every level.
    { model } = init
    { country } = model
    { state } = country
    { city } = state
    { people } = city
in
    { init
        | model =
            { model
                | country =
                    { country
                        | state =
                            { state
                                | city =
                                    { city
                                        | people = "John" :: people
                                    }
                            }
                    }
            }
    }

Upvotes: 3

wintvelt
wintvelt

Reputation: 14101

It is usually better to make the record structure flatter if you want to do stuff like this.
Typically:

  • nested records are OK if your manipulation is also nested: if you do have a separate updating function for each level. And/or if you only want to update the nested record at its own level.
  • if you need root level access to some nested record, then flat is better.

What I tend to do is store everything at root level, and include IDs for reference. For your example this would look something like this:

type alias Model =
  { persons : Dict PersonID Person
  , cities : Dict CityID City
  , states : Dict StateID State
  , countries : Dict CountryID Country
  }

This structure allows you full access from root level to all entities. So adding a person would be:

  { model 
  | persons =
      model.persons
      |> Dict.insert newPersonID newPerson
  }

To get all cities in a country would be a bit more work now BTW:

citiesInCountry : Model -> CountryID -> Dict CityID City
citiesInCountry model countryID = 
    let
      statesInCountry =
        model.states
        |> Dict.filter (\id state -> state.countryID == countryID)
    in
      model.cities
      |> Dict.filter (\id city -> Dict.member city.stateID statesInCountry)

So it kind of depends which operation is more frequent in your app:

  • if you have a lot of operations like "get all grandchildren", then the nested approach could be better
  • if you have a lot of "update this grandchild", then the flat alternative might be better suited

Upvotes: 1

Simon H
Simon H

Reputation: 21005

There is https://github.com/evancz/focus but note the small print. Could you work with a different data structure instead, such as

Dict ( String, String, String ) Int

Then

addOne country state city = 
  Dict.update (country, state, city) 
     (\v -> case v of 
               Just v_ -> v_ + 1
               Nothing -> 1)
     database

Upvotes: 0

Related Questions