Best data structure for a collection of UI elements?

Most of my Elm apps have some kind of ordered collection of sub-models (for lack of a better term), and a lot of Msg types pertaining to updating each sub-model. I think this is pretty typical, but the ways in which I implement it don’t feel very nice:

type alias Model = { items : List Item }

type alias Item = { uid : Int, value : String }

type Msg =
    ItemMsg Int ItemMsg 

type ItemMsg = ValueChanged String

update msg model =
    case msg of 
        ItemMsg uid subMsg ->
             case subMsg of
                  ValueChanged value ->
                          (updatedItems, cmds) =
                                      (\item ->
                                         if item.uid == uid then
                                            ({ item | value = value}, Cmd.none)
                                            (item, Cmd.none)
                                   |> List.unzip
                          ({ model | items = updatedItems}
                          , cms |> Cmd.batch |> (ItemMsg uid)

Basically what I hate about this is List completely sucks as a keyed collection, and I’m sure that my method of updating a single element with a Cmd has to be totally inefficient. However, I feel trapped into using it since I need to preserve order, and map the whole thing to a List every time it changes to generate the HTML anyway.

Here are the downsides of other Elm collections:

Array: Slow to convert to a list. Need unchangeable UID’s, not indexes. Indexes can result in race conditions.

Dict: Lets me use UID’s, but doesn’t preserve ordering. Will always be sorted by UID, so rendering requires converting the values to a List and sorting.

1 Like

If you store your items by id in a Dict, then you can store their order as a List of ids.

type alias Model =
    { items : Dict Int Item
    , itemOrder : List Int

Now updating items and itemOrder are completely separate. (You could even store multiple orders.)

update msg model =
    case msg of 
        ItemMsg uid (ValueChanged value) ->
            { model | items = model.items |> Dict.insert uid value }

When you need a list of items, you can map over the list, getting each item from the dict. And if you can safely omit any unfound items, then you can map over itemOrder with List.filterMap as a convenience.

itemList  : List Item
itemList  =
    itemOrder |> List.filterMap (\uid -> Dict.get uid items)

I like this solution! It feels good to code at least, I’d love to be able to measure the speed differences though! At least I am guessing it should be faster for updates, which is all I am really going for, but I have no idea what kind of a trade-off the List conversion will turn out to be for my view function.

If you can use list position to index the items (and I have never met a case where this is impossible), you could use a variation of elm-list-extra’s updateAt.

Performance is probably a bit better in a theoretical sense, but it really doesn’t matter at this level. The main advantage is style - you don’t have to carry around and then drop all those Cmd.nones.

updateAtWithCmd : Int -> (a -> (a, Cmd msg)) -> List a -> (List a, Cmd msg)
updateAtWithCmd index fn list =
    if index < 0 then
        (list, Cmd.none)
            head =
                List.take index list

            tail =
                List.drop index list
            case tail of
                x :: xs ->
                       (newx, cmd) = fn x
                    (head ++ newx :: xs, cmd)

                _ ->
                    (list, Cmd.none)

Also - I take it you wrote out that update as a single function just out of convenience? It would be much more manageable if you pulled it out into separate functions, as you probably realize. Something like:

update msg model =
    case msg of 
        ItemMsg uid subMsg ->
                 (newItems, cmd) =
                     updateItem uid subMsg model.items
                 ( { model | items = newItems }
                 , cmd |> ItemMsg

updateItem : Int -> ItemMsg -> List Item -> (List Item, Cmd ItemMsg)

Yeah I always pull it out into a helper, it just feels stupid and bad to write that code.

Also, I use UID’s, not indexes, to avoid race conditions, which adds an extra layer of complexity.

Hi, There are data structures that work as a Dict but preserve order like a List. Here is one implementation from the package site that was not too hard to find:

Elm is not concurrent, so does not have race conditions. Perhaps you could give an example of how you think indexes could end up in a race condition? (I think it is sort-of possible if you have functions capturing continuations in your model, but this should be avoided).

Long running tasks, such as persistence and API loads, have to return and update the correct element! If the element order changes, you may update the wrong element.

Different persistence models may change this in different ways.

Long running tasks, such as persistence and API loads, have to return and update the correct element! If the element order changes, you may update the wrong element.

Any state changes, even asynchronous ones “from the outside”, results in the view function being run again with the new state. So as far as setting up the event handlers on a list of elements, it’s safe to use List indexes.

It’s true that things like (ItemMsg index) can be problematic if you can have > 1 async updates going on concurrently. It’s a good point, I can see why you would use some UID in that case. I guess I’ve never had a persistence strategy where I’ve encountered this, but I can see how it would be possible.

Ok, I see your point. I would not use an index into a data structure as an id, if that index can change over time. But perfectly safe to use an Int counter, and bump it by one to assign ids to elements.

To tell you the truth I don’t know how often it can happen in practice, but in some of my apps reordering or deletion is slow and costly, and could easily still be going on while the user does something that fires off a quick small update. I could also make it read-only for these events, or queue up changes, but I’d rather KISS.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.