As an Elm developer, you’re probably familiar with the forward function application operator (|>)
.
And you may have used the NoRedInk/elm-decode-pipeline
package to create JSON decoders like so:
userDecoder : Decoder User
userDecoder =
decode User
|> required "id" int
|> required "email" (nullable string)
|> optional "name" string "N/A"
But have you really taken the time to sit down and think about what the |>
is really doing in this case?
In this post, I’ll show you how to use the |>
operator to create pipelines and to simplify your existing Elm code.
You’ll end up with a technique to simplify the code in your update
function. Here’s a teaser:
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Increment ->
(model, Cmd.none)
|> updateCounter 1
|> recordAnalyticsEvent "counterIncremented"
|> addSuccessMessage "You incremented the counter!"
First, let’s quickly review the forward function application operator.
It takes the value on the left and applies it to the function on the right (follow along in elm-repl
if you wish):
import String exposing (..)
"Hello" |> isEmpty -- False
"" |> isEmpty -- True
Pretty simple, right? Next, let’s follow this pattern to manipulate a string:
"Hello everyone" |> left 5 |> toLower |> append "I say " -- "I say hello"
Now we’re passing the value returned from each function to the next, starting with the base value of "Hello everyone"
and ending with the final value of "I say hello"
.
Let’s examine what enables us to do this by looking at the type signatures of each of the functions we’re using:
left : Int -> String -> String
toLower : String -> String
append : String -> String -> String
While all three type signatures differ, they have one important thing in common: they all end with String -> String
.
To generalize this concept, we can say that if we have a function that returns a type a
, then we can create pipeline functions
for it where each pipeline function type signature ends with a -> a
.
Let’s take this knowledge and apply it to the update
function in The Elm Architecture.
We’ll start by examining its signature:
update : Msg -> Model -> (Model, Cmd Msg)
Since update
returns (Model, Cmd Msg)
we can create pipelines for it with functions that end with (Model, Cmd Msg) -> (Model, Cmd Msg)
.
Here’s a (really simple) example:
incrementCounter : (Model, Cmd Msg) -> (Model, Cmd Msg)
incrementCounter (model, cmd) =
({ model | counter = model.counter + 1 }, cmd)
And here’s how to use it:
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Increment ->
(model, Cmd.none) |> incrementCounter
Unfortunately, incrementCounter
is very brittle, as it can only do one thing: increment the counter
field of the Model
by exactly one.
Since our app will also feature a decrement button, let’s change incrementCounter
to take a parameter and rename it:
updateCounter : Int -> (Model, Cmd Msg) -> (Model, Cmd Msg)
updateCounter delta (model, cmd) =
({ model | counter = model.counter + delta }, cmd)
Now we can add the Decrement
action to our update
function:
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Increment ->
(model, Cmd.none) |> updateCounter 1
Decrement ->
(model, Cmd.none) |> updateCounter -1
While this example is quite trivial, you can apply it to a variety of use cases to create reusable building blocks.
Let’s say you have an app that calls an external service to record analytics events and presents the user with a list of disappearing success and error messages. You might write something like:
recordAnalyticsEvent : String -> (Model, Cmd Msg) -> (Model, Cmd Msg)
recordAnalyticsEvent eventName (model, cmd) =
let
analyticsCmd = -- A bunch of code to call the analytics server
in
(model, Cmd.batch [cmd, analyticsCmd])
addSuccessMessage : String -> ( Model, Cmd Msg ) -> ( Model, Cmd Msg )
addSuccessMessage m ( model, cmd ) =
let
removeUserMessageCmd = -- Cmd to remove the message after 5 seconds
in
({ model | userMessages = SuccessMessage m :: model.userMessages }
, Cmd.batch [ cmd, removeUserMessageCmd ])
And then in your update
function:
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Increment ->
(model, Cmd.none)
|> updateCounter 1
|> recordAnalyticsEvent "counterIncremented"
|> addSuccessMessage "You incremented the counter!"
Decrement ->
(model, Cmd.none)
|> updateCounter -1
|> recordAnalyticsEvent "counterDecremented"
|> addSuccessMessage "You decremented the counter!"
...
Great! Now we have a bunch of reusable functions that can be sequenced to build complex update logic in our Elm application! Any time we need to record another analytics event or add a success message, we simply just pipe our update return value to the functions we created above.
Bonus Round
Let’s explore how we can create pipelines with the Maybe
type in Elm using our string manipulation example above:
Just "Hello everyone" |> map (left 5) |> map (toLower) |> map (append "I say ")
Notice that the only difference is the map
call before each function call.
I’ll leave the reasoning to the reader - but a good place to start is the Elm documentation for Maybe.
In Haskell, Maybe
would implement the
Data.Functor
typeclass, and map
would actually be fmap
.
Elm doesn’t use typeclasses (in order to keep things a little more simple), but if you’re interested in exploring the concept further,
check out the Haskell Functor documentation.