[Beginner] Finding which sub-model to update

Let’s say we have a Counter module with Increment | Decrement | Reset messages and a model that holds an Int.
We also have a CounterList module which has as a model a List of Counter models.
Finally, at the top level, we have our App module with a model that holds 3 distinct CounterLists.

User presses Increment button on a Counter in List 2. How does the App’s update method know which sub-model to update?

What are the best practices here?
Augment the Counter model with some kind of Id and perform a search by Id in each of the 3 App’s lists (which also means that each Counter should have a globally unique Id)?

Thank you in advance.

1 Like

There are a bunch of ways to approach this! The most straightforward is to separate message handling for the parent and child. You may have already done this! So what remains is to fill in the data structure.

I’d use Array for this. Until 0.19, you’ll want to use Skinney/elm-array-exploration for this, as core has some bugs. You’ll use it a little like this:

module CounterList exposing (..)

import Counter exposing (Counter)
import Array.Hamt as Array

type alias CounterList =
  Array Counter

type Msg
  = CounterMsg Int Counter.Msg

update : Msg -> Model -> Model
update msg model =
  case msg of
    CounterMsg index counterMsg ->
      model
        -- let's look up the counter at that index
        -- first. This will give us `Maybe Counter`.
        |> Array.get indexFromMsg
        -- next, we'll update the counter inside
        -- the `Maybe`. I'm assuming `Counter.update`
        -- has the same signature except with a counter
        -- instead of `Model`. After this we still have
        -- `Maybe Counter`.
        |> Maybe.map
          (\counter -> Counter.update counterMsg counter)
        -- now let's set the updated counter inside our array.
        -- after this, we have `Maybe (Array Counter)`.
        |> Maybe.map
           (\counter -> Array.set indexFromMsg counter model)
        -- we need to unwrap our array from the Maybe, so we need
        -- to provide a default. I'm just going to not care if we
        -- were given a bad index, and return the original model.
        -- you may want to do something different here.
        -- If that's the case, stick this whole thing in a `let`
        -- and pattern match on the `Maybe (Array Counter)`.
        |> Maybe.withDefault model

If that doesn’t answer your question, could you share what you’ve got so far and we can work on it together?

This data transformation looks daunting at first, but give it a little time. These things quickly become second nature. The compiler will help you here, and so will we. :slight_smile:

I think Brian missed the fact that you said you had multiple counter lists, so extending his answer to include that, the Msg type would be:

type Msg = CounterMsg WhichCounterList Int Counter.Msg

type WhichCounterList = ListA | ListB | ListC

Or if you want to make things more general, you could use an Int or String instead of defining a new type to identify which list the counter is in. (If your model has a record with a field for each CounterList, then I’d use a type like this; if it’s an Array of CounterLists, I’d use Int, and if it’s a Dict of CounterLists, I’d use String.)

I’d also tend to structure my update to use more small functions vs having a big pipeline:

update : Msg -> Model -> Model
update msg = 
    case msg of
        CounterMsg whichList whichCounter counterMsg ->
            updateCounterList whichList
                (updateCounter whichCounter
                    (Counter.update counterMsg)
                )

updateCounterList : WhichCounterList -> (List Counter -> List Counter) -> Model -> Model

updateCounter : Int -> (Counter -> Counter) -> List Counter -> List Counter

Wow thanks brian. Intuitive and succinct data transformation for the win!

I was looking for a way to nest modules arbitrarily deep and not pollute their models with IDs or any other data that would leak information about how these modules are used by parents.

I just had a read of https://github.com/rtfeldman/elm-architecture-tutorial and the use of Signal.Address made a lot of sense in this case. Unfortunately, it’s no longer present in 0.18. I’m unsure what pattern I should be using instead. But I’m definitely a fan of the parent keeping track of IDs of children separately and sending each child an opaque decorator or mapping function that the child would use for all of it’s messages. Maybe sending is the wrong method, and instead the parent’s view function would just map the Html Msg returned by each child with a closure unique to that child model (decorate it with a unique ID).

avh4 makes sense. do you see any problem with the following (if I were to take it one step further):

App.elm:
type Msg = ...| ... | CounterListMsg Int CounterList.Msg

CounterList.elm
type Msg = ... | ... | CounterMsg Int Counter.Msg

Counter.elm
type Msg = Increment | Decrement

This way each module is oblivious to its parent and we can nest them arbitrarily deep without writing crazy amounts of pattern matching code in App.elm

Yep, that seems reasonable.

Though if CounterList doesn’t do much more than contain counters, you might want to consider making it a module that only has reusable functions in it (and not have a Msg and update if it doesn’t really need it). For long-term projects, having lots of nesting is less helpful that it can initially seem. See the “Scaling” part of the Elm Guide:

https://guide.elm-lang.org/reuse/

Whooooops! Good catch, thanks!