TL;DR - I eliminated a bunch of update code by moving it into the view. It was a good idea. Trust me.
I have an application that manipulates lists quite a bit. The user can add things at the beginning, end, move things around, etc. Each item in the list is one of a few different element types, each with their own set of input fields. I implemented this a long time ago, when I was very new to Elm, in the naive and obvious way - msg types for every field of each element type, and msgs for all the list manipulation operations. There were a lot of them.
This was a painful file of code that was very difficult to understand if only for its sheer size. Something like 700 lines of case statements doing record updates and handing Maybe cases that could never happen. (Like the UpdateElement Int UpdateElementMsg
which needed to account for the element at the given index not existing, or not being the type the UpdateElementMsg was for - neither of which would ever actually happen.)
So I tried to fix it now that I have much more experience with Elm. I ended up deflating 700 lines to around 80. The code for the entire list editing webpage nearly fits in my editor’s minimap now, which is neat. I figured I share the two techniques I used - maybe they’ll help someone else.
The first thing was to switch the view functions for each element type from viewElement : Element -> Html Msg
to viewElement : Element -> Html Element
. This added a few \input -> { element | text = input }
lines to each element view function, but eliminated a ton of Msg types and update case statements.
So that cleared up most of the Element updating code, but the list manipulation code was still an extra-large plate of spaghetti. So I wrote this function:
viewList :
{ onSet : List data -> msg
, viewItem :
{ item : data
, set : data -> msg
, delete : msg
, insertAfter : data -> msg
, insertBefore : data -> msg
, moveBackward : msg
, moveForward : msg
}
-> viewItemResult
, viewList :
{ items : List viewItemResult
, append : data -> msg
, prepend : data -> msg
}
-> viewResult
}
-> List data
-> viewResult
viewList config list =
config.viewList
{ append = List.singleton >> (++) list >> config.onSet
, prepend = (::) >> (|>) list >> config.onSet
, items =
List.indexedMap
(\index item ->
config.viewItem
{ item = item
, set = List.Extra.setAt index >> (|>) list >> config.onSet
, insertAfter = insertAt (index + 1) >> (|>) list >> config.onSet
, insertBefore = insertAt (index + 1) >> (|>) list >> config.onSet
, delete = config.onSet (List.Extra.removeAt index list)
, moveBackward = config.onSet (List.Extra.swapAt index (index - 1) list)
, moveForward = config.onSet (List.Extra.swapAt index (index + 1) list)
}
)
list
}
That function does in 18 lines what used to take about 200. (I know what you’re thinking. “(::) >> (|>) list >> config.onSet
. Really?” I swear it’s totally easy to read after understanding the basic pattern of >> (|>) x
. Replace it with a lambda if you want.)
Now I can write the list view function like this (where the viewUnit
function has the type Unit -> Html Unit
):
type Unit
= Text { content : String }
| Audio { url : String }
| Video { url : String }
view =
viewList
{ onSet = SetList
, viewItem =
\{item, moveBackward, moveForward, delete, set} ->
Element.tile
{ header =
Element.horizontal
[ Element.upChevron moveBackward
, Element.downChevron moveForward
, Element.deleteIcon delete
]
, body =
[ Html.map set (viewUnit item)
, ... some controls for inserting elements above and below ....
]
}
, viewList =
\{ items, append } ->
Element.vertical (
items
++
[ Element.button "Add Text" (append (Text { content = "" }))
, Element.button "Add Audio" (append (Audio { url = "" }))
, Element.button "Add Video" (append (Video { url = "" }))
]
)
}
My main takeaway from this exercise is “Msg types can be whatever the heck you want them to be, and probably should be.” (Obligatory incantation to keep the best-practice gnomes happy: Except functions. Never put functions in your messages.)
P.S. I might have code-golfed a bit too much with that viewList
function. At least it was fun to write.