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.