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:
What is the “best” way or most idiomatic way to do this?
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.
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 …
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.
@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?
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.
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.
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.