List, Array, or Dict for a series of dynamic checkboxes

I’m working on a project to learn Elm where I have a list of tags that the user can create and add and then use to filter (they are all checkboxes). At first, I had created these as a List. However, I realized when I went to update them, I needed a key in order to know which checkbox to update when they were being checked.

I refactored to an Array, and overall this feels like a better solution. The only odd part is in order to do an update I have to do a get -> set, which leaves a Maybe that I have to default (with something that theoretically should never be used). In addition, I have to convert the Array to a list in a lot of places.

All of this left me with two questions:

  1. What is the “best” way or most idiomatic way to do this?
  2. When using an Array, what is the cost of converting to and from a list? (I’m not particularly concerned with performance, I’m more interested in understanding what’s happening under the hood)

Can you share the code and/or what the UI should look like? I’m not clear where you need to convert to a list. On the other hand, from your description, it seems perfectly fine to model the tag-checkbox things as a list, you can use functions like updateAt in elm-community/list-extra. Not the most efficient perhaps but assuming the list will never be very long it doesn’t matter.

Here is an Ellie:

https://ellie-app.com/4nrZ6mhdHBWa1

This solution works fine, I’m just not sure if this is the most elegant or correct way to do things (i.e. am I missing something).

I’ve extracted some of the excerpts where I’m using Array below:

defaultTag : Maybe Tag -> Tag
defaultTag t =
    Maybe.withDefault (Tag "UNKNOWN!" False) t

update : Msg -> Model -> Model
update msg model =
    case msg of
        TagChecked index checked ->
            let
                t =
                    defaultTag <| Array.get index model.tags
            in
            { model
                | tags = Array.set index { t | checked = checked } model.tags
            }

-- Inside the view
                , row
                    [ width fill, height fill, paddingXY 5 0 ]
                    [ column
                        [ width fill
                        , height fill
                        , Background.color <| rgb255 255 255 255
                        , Border.rounded 3
                        ]
                        (Array.toList <|
                            Array.indexedMap
                                tagView
                                model.tags
                        )
                    ]

As for how to model it, I think you did it almost perfectly. The only thing I would change is the TagChecked update.

Array.get shouldn’t really return Nothing, but it can happen if two messages are sent right after each other and one e. g. removes an item such that the second message’s index becomes invalid.
What do you want to do, if Array.get actually returns Nothing? Well, nothing. Just leave the model as is.
This means that you can match with a case on the result of Array.get. If it is Just tag, then you can update the tag in the model. If it is Nothing, just return the old model.

As for your question on how Array. toList works, I have to pass …

Based on the UI, I would model this as a Dict String Bool. I think the dictionary would simplify your code, unless I’ve misunderstood something :sunny:

I’ve also used List (Id, Data) where Id is a type alias to Int or String depending on the data, and Data is whatever I’m storing.

I believe it is not a good idea to rely on data structure indexes in the messages since if you remove or add things you can get messages with the “old” id, and then have a hard to track logic bug. YMMV though.

I also used in 0.18 a package called sorted dict, or ordered dict, or dictlist, that was very useful, but I don’t see a good implementation for 0.19 yet.

Seems like this is a job for Dict, really. But hey, as long as it works…

Converting an Array into a List is really just Array.foldr (::) [] array. It’s pretty fast, but not free.

Doing a better pattern match on the Maybe makes a lot of sense. Thanks!

@opvasger @joakin @robin.heggelund I actually first modeled it as a Dict. I think what I struggled with was the view rendering. When I did a key/value map over the Dict I was getting another Dict as a result (which I guess makes sense). In retrospect, I’m guessing the right way would be to do the same, and then just call .values

I believe it is not a good idea to rely on data structure indexes in the messages since if you remove or add things you can get messages with the “old” id, and then have a hard to track logic bug. YMMV though.

I thought that Elm, being single-threaded with seemingly atomic updates, would never run into this case. I.e. the only way an element would be removed is through an message and an update to the model, which would re-render the view and thus re-index all the future messages. Is that not correct?

You can use Dict.toList and then map over the list :sunny:

I don’t recall exactly how those race conditions can occur but something about updates being able to run multiple times before the view renders, since the views render with requestAnimationFrame, every 16ms.

I’ve seen it myself in the past, don’t remember exactly with what, but if you look around in some of the canonical examples you will see that ordered lists of items have their own id rather than relying on the array indexes.

For example, see evancz/todomvc and notice how the entry has an id field that is an Int.
That is the id that is used for editing and updating the entries and when triggering events [1] [2] [3].

Maybe someone else can provide a bit more clarity around why this is preferred and when it can be problematic to rely on just the indexes as I don’t remember or can find what to link to right now.

Got it, thanks. I think in most typical scenarios there would probably be an ID from the backend in any case!

This post talks a bit about it:

It is not guaranteed that the view will have the latest model always, and you should avoid having logic on it.

Picture that on your model you remove an entry, and the user generated a message on one entry after that one. The message will come from the old model indices. In update you will get a message with an id referring to a different thing than what the user thought because the items shifted.

So if your collection will have arbitrary additions or removals I would personally consider using indexes to send messages on the view dangerous.

1 Like

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