Tricks with case expressions in Elm
3 tricks to get more out of case expressions
The case..of
expression is at the core of many elm app. All of my update
functions start with one, like:
update msg model =
case msg of
AddItem ->
...
EditItem itemId ->
And (despite a desire to use .map wherever I can) my code still has plenty of
case maybeStuff of
Just stuff ->
...
Nothing ->
...
But besides iteration over a Msg
, or any other Union Type, there are some other nice things you can do with the case
expression, which makes it much more versatile.
#1. Use case expressions to operate on lists
With the case
expression you can also disect lists into separate branches. If you want to do different things with a list if it has zero, one or many items, you don’t need List.isEmpty
or List.length
. Instead, you can build a case
expression for it. Like this:
case myList of
[] ->
...
[ item ] ->
...
items ->
...
The last branch covers all cases where there is more than one item in the list. The branches need to cover all possible states of the list, but fortunately, the compiler has your back, and will warn you if you missed one.
A more advanced application is to combine it in a recursive function. An example is below. This function compares the first two items in a list, and combines them if the second one is actually a child item of the first. After that, it (recursively) calls itself on the remainder of the list.
rollUp : List Record -> List Record
rollUp items =
case items of
-- there are at least 2 items left
a :: b :: rest ->
-- the first is the parent of the second
if b.parent == a.id then
-- combine the first two and make resursive call
{ a | children = b.content :: a.children }
:: rest
|> rollUp
else
-- keep the first and
-- make recursive call with second item and rest
a :: (rollUp <| b :: rest)
other ->
-- there are 0 or 1 items in the list
-- return list unchanged
other
This pattern I learned from going through the source code for the List.Extra package.
Of course you can simply install this package and use its greatness. But for understanding the pattern, the source code is a great read, and well documented.
#2. Case expressions for ad hoc combined variables
Sometimes when your update
gets a Msg
with new data, you may want to compare the new and the old value, before you do the update. In my case, this was to update some other variable in my model, and trigger animations, or to trigger some conditional message to the user.
Especially when the new and the old values are Union Types, comparing old and new values may become quite verbose.
Fortunately, you can create an ad hoc Tuple (or List or any other structure) of the old and the new value and put it in a case expression:
case ( oldValue, newValue ) of
...
and make branches for those.
Below is a simple example: the function userMessage
returns an appropriate user message, depending on the type of change from the old to the new status.
type Status
= Connected
| DisconnecteduserMessage : Status -> Status -> Maybe String
userMessage oldStatus newStatus =
case ( oldStatus, newStatus ) of
( Connected, Disconnected ) ->
Just "Sorry, you were disconnected just now" ( Disconnected, Connected ) ->
Just "Hooray, you are connected!"
( _, _ ) ->
Nothing
#3. Case expressions with literals and variables
Any lower cased name in your case branches is used for pattern matching. So if you have a branch SelectWidget id ->
, then you can use the id
in that branch to update the right widget in your model.
However, you can also put literals like 4
, or "users"
in your branches to match to specific values.
So if you have an Int
and you want different actions for 0,1, any other value, you could make a simple case expression for this:
case someInt of
0 ->
...
1 ->
...
_ ->
...
A good real-life example of this is a custom url parser. The function below splits a url like /items/14
into a list of string segments like [ "items" , "14" ]
. The actual parsing is done by a case expression on the segments.
simpleParse : String -> Route
simpleParse url =
let
segments =
String.split "/" url
|> List.filter (\s -> s /= "")
in
case segments of
[] ->
Home
[ "items" ] ->
Items
[ "items", id ] ->
ItemDetail id
_ ->
Home
Credits for this pattern to Josh Adams, in whose Time tracker Elm app I first saw this pattern used.
Not for every case
Even with all its goodness, there are still situations where a case expression will not work, and you will need to use different pattern.
No constants in case.
You can use literals, including Union Typed variables, in case expressions. But you cannot use constants.
-- THIS WORKS
case language of
"Spanish" ->
...
-- THIS DOES NOT WORK
localLanguage = "Spanish"case language of
localLanguage ->
...
The second one does not work because elm uses the localLanguage
in the branch as a pattern matcher: if language
matches the general pattern that it is a “thing that could be anything” (basically always), it will assign the value of that part insidelanguage
(in this case the whole language) to the new variable localLanguage
.
For constructed types we use this all the time in branches like Just user ->
to do something with the user
inside a Maybe
, or in the pattern a::b::rest
in the earlier example.
Elm always uses lower case variables in case
branches for pattern matching, and only allows literals. Elm does not allow case
expressions on constants or assigned values. Probably this has to do the guarantees on the compiler needs to make that your branches cover all possible cases.
No pattern matching on record contents.
In case expressions, you can use literals inside strong types (using its type constructor), but I have not yet found a way to use the literals on record type constructor.
-- THIS WORKS
type Locale = Locale String String
case someLocale of
Locale "es" "es" ->
...
-- THIS DOES NOT WORK
type alias Locale = { language : String, country : String }
case someLocale of
Locale "es" "es" ->
...
Still, I hope the tricks above will help you get more out of case
expressions, and help you make your code more readable too.