Elm patterns

A collection of common patterns for Elm.

Type blindness

Type blindness is when you have several values of the same type that could get mixed up.

Example 1

type alias User =
  { firstName: String, lastName: String }

Both attributes are String. It is easy to mix them up when receiving external information.

Example 2

priceInDollars : Float
priceInDollars = 2.0

priceInEuros : Float
priceInEuros = 1

Both values are Float. There is nothing preventing us from doing a non sensical operation like priceInDollars + priceInEuros.

Pattern

In this case consider wrapping values in a unique type.

type Dollar = Dollar Float

priceInDollars : Dollar
priceInDollar =
  Dollar 2.0

You will need to unwrap values later, which can get tedious, so use this when there is a good potential of mixing up values.

Minimize boolean usage

Boolean ambiguity can lead to loss of intent and loss of information in our code, resulting in ambiguous logic. To avoid this issue, we should prefer self-documenting alternatives whenever the intention is unclear.

This pattern is inspired by Jeremy Fairbank's "Solving the Boolean Identity Crisis" talk and series of articles, where he explains the rationale and the pattern in great details, if you want to delve deeper.

Boolean Ambiguity

Boolean arguments can make code confusing and harder to maintain by hiding the intent of code.

Anti-Pattern 1

What is the significance of the True value here without looking up the definition of bookFlight?

bookFlight "ELM" True

Pattern 1

We can clean up the bookFlight function by replacing the boolean arguments with a Custom Type:

type CustomerStatus
    = Premium
    | Regular
    | Economy

Now calls to bookFlight declare the intent of code because we pass in the CustomerStatus directly:

bookFlight "ELM" Premium

Boolean Blindness

Returning Bool from a function can lead to boolean blindness. This happens because we get a value that the type system cannot use for enforcing further logic in the program.

Anti-Pattern 2

For example:

time =
    if isValid formData then
        submitForm formData

    else
        showErrors formData

In this snippet isValid only returns a Bool. We can call submitForm with the original formData. The compiler wouldn't complain if we swap the if-else branches.

Pattern 2

Replace boolean return values with custom types to eliminate boolean blindness and leverage the compiler for safer code. In this example we do it with a Result Error Type:

time =
    case isValid formData of
        Ok validFormData ->
            submitForm validFormData

        Err errors ->
            showErrors errors

In this case submitForm is a function that can only be called with valid form data.

Named arguments

There are times when the order of arguments for a functions can be ambiguous. For example:

isBefore : Date -> Date -> Bool

What is the subject date here and the comparison date?

As we usually put the data at the end (for pipelines) we might think that the subject is the second date, but maybe is not. This is vague and error prone.

In this cases is better if we ask for a record as argument:

isBefore : { subject: Date, comparedTo: Date } -> Bool

This might not be pipeline friendly, but it is a lot more precise and hard to get wrong.

Wrap early, unwrap late

To avoid type blindness, you probably want to wrap your values in unique types.

When you do this it is a good idea to wrap your types as early as possible. E.g. when decoding values from external sources.

Then try to unwrap as late as possible.

Example anti-patterns

displayPriceInDollars : Float -> String
displayPriceInDollars price =
    "USD$" ++ String.fromFloat price

There is nothing stopping us from passing something that is not dollars here (e.g. we might use Euros).

calculateTotalPrice: List Float -> Float

We could send a list of mixed currencies e.g. dollars and euros.

Pattern

type Dollar = Dollar Float

displayPriceInDollars : Dollar -> String
displayPriceInDollars (Dollar price) =
    "USD$" ++ String.fromFloat price

This enforces that we use the Dollar type as much as possible.

Unwrap Maybe and Result early

If you have a Maybe, Result or RemoteData it is often a good idea to try to unwrap those as early as possible.

Anti-pattern

userCard : Maybe User -> Html Msg
userCard maybeUser =
   div []
        [ userInfo maybeUser
        , userActivity maybeUser
        ]

In here both userInfo and userActivy get a Maybe User. Meaning that will need to unwrap this value several times in your views.

Pattern

userCard maybeUser =
    case maybeUser of
        Nothing ->
            div [] [ ... ]

        Just user ->
            div []
                [ userInfo user
                , userActivity user
                ]

Here the sub views take a User, this makes most of your views easier to write and test.

Make impossible states impossible

Elm has a great and expressible type system. This type system allows us to avoid having impossible states in our application.

Anti-pattern

A common pattern is to have a boolean attribute to show a loading spinner while data is loading. e.g.

type alias Model =
    { isLoading: Bool
    , data: Maybe Data
    }

But in this type it is possible to have something like isLoading = false and data = Nothing. What is the meaning of this? This is probably an impossible state that should never happen.

Pattern

With Elm you can represent your types in ways that don't allow for impossible states. e.g

type RemoteData
    = Loading
    | Loaded Data

type alias Model =
    { data : RemoteData }

Here is an excellent talk about this.

Parse don't validate

When we have external data (e.g. user input or remote data) it is a common pattern to validate this data before using it.

Anti-pattern

A common approach is to ask if the data is valid and then use it e.g.

type alias UserInput =
    { name: Maybe String
    , age: Maybe Int
    }

isValidUser : UserInput -> Bool

The problem with this approach is that after doing this you still have a UserInput with can still hold invalid values.

Pattern

A better approach is to "parse" your input and return a known valid type. e.g.

 type alias UserInput =
    { name: Maybe String
    , age: Maybe Int
    }

type alias ValidUser =
    { name: String
    , age: Int
    }

validateUser : UserInput -> Result String ValidUser

In this way you ensure that you have a valid type to work with later on. Some examples where this is useful:

  • Validating user input
  • Parsing JSON from external sources

The builder pattern

When we need to pass many arguments to a function we might have something like

module Button exposing (..)

type alias Args =
    { isEnabled : Bool
    , label : String
    , hexColor : String
    , ...
    }

btn: Args -> Html msg

In the caller module:

import Button

Button.btn { isEnabled = True, label = "Click me", ....}

The problem with this is that each time we add an argument to Args we need to change every single place where we call this function.

Pattern

With builder pattern we build the arguments with the minimum necessary information, then modify the arguments if we need to.

module Button exposing (..)

newArgs: String -> Args
newArgs label =
    { isEnabled = True
    , label = label
    , hexColor: "#ABC"
    , ...
    }

withIsEnabled : Bool -> Args -> Args
withIsEnabled isEnabled args =
    { args | isEnabled = isEnabled }

btn: Args -> Html msg

This modules exposes a function to create the initial arguments and a series of function to modify the arguments (commonly using with as prefix).

Then the caller module uses this:

import Button

aButton =
    Button.newArgs "Click me"
        |> Button.withIsEnabled False
        |> Button.withHexColor "#123"
        |> Button.btn

The advantage of this is that adding new arguments to Args doesn't require us to change every caller.

As test factories

This pattern is also very useful for tests. Similar to test factories in many languages. For example if we were testing a User, we could start with a basic user and then use the builder pattern to modify attributes for different tests.

Arguments list

The builder patterns gives us to build configuration. We could also use plain lists for this:

view =
  shape
    [ scale 0.5 0.5 0.5
    , position 0 -6 -13
    , rotation -90 0 0
    ]

Our module exposes a series of functions that return a common type e.g.

scale : Float -> Float -> Float -> Attribute

rotation : Int -> Int -> Int -> Attribute

Using that we can build a List Attribute and pass it to a function as configuration.

This is the pattern used in elm/html, elm/svg, elm-css, elm-ui.

  • This pattern is best used with Opaque types. As we don't usually want the caller to be able access the returned type (e.g. Attribute).
  • This pattern is best when all arguments are optional as we cannot avoid having the caller pass an empty list.

Type iterator

When we have a list of all the variants in our custom type, sometimes that list can get out-of-sync when we add a new variant.

module Color exposing (Color(..), all)

type Color
    = Red
    | Yellow
    | Green
    | Blue -- Recently added

all : List Color
all =
    [ Red, Yellow, Green ]
    -- Forgot to add Blue here

It would be nice if the compiler could remind us. Especially so if there are many variants.

There are at least two solutions to this.

The first one is using an elm-review rule NoMissingTypeConstructor that automatically checks lists of custom type constructors named all* and warns when they get out of sync.

The second one is to get the compiler to remind us of a missing variant by building our list with a case statement.

module Color exposing (Color(..), all)

type Color
    = Red
    | Yellow
    | Green
    | Blue -- Recently added

all : List Color
all =
    next [] |> List.reverse

next : List Color -> List Color
next list =
    case List.head list of
        Nothing ->
            Red :: list |> next

        Just Red ->
            Yellow :: list |> next

        Just Yellow ->
            Green :: list |> next

        Just Green ->
            list -- return the list as is on the final variant

        -- Blue is still missing, but now, the compiler will complain
        -- until we add it here

Important: We should never use wildcard matching _ in the next function. It would prevent the compiler from detecting new variants.

Conditional rendering

Many time we want to conditionally render an element. For example, sometimes we want to render a banner, sometimes we don't. Here are some common patterns for this:

Using Maybe

We can write our HTML like:

[ Just headerElement
, maybeBanner showBanner
, Just content
, Just footerElement
] |> List.filterMap identity

maybeBanner showBanner =
  if showBanner then
     Just bannerElement
  else
    Nothing

In this pattern we create a list of Maybes, and then use List.filterMap identity to remove the Nothings.

Using a no-op

We can also achieve this using:

[ headerElement
, maybeBanner showBanner
, content
, footerElement
]

maybeBanner showBanner =
  if showBanner then
     bannerElement
  else
    text ""

In this case text "" is a no-op (No operation). So nothing gets rendered. To achieve the same for attributes we can use class "".

Using list concatenation

And we could also write our HTML like:

( [ headerElement ]
++ maybeBanner showBanner
++ [ content, footerElement ]
)

maybeBanner showBanner =
  if showBanner then
    [ bannerElement ]
  else
    []

In this case we use list concatenation to assemble the HTML. When we don't need to render the banner we can return an empty list.

The railway pattern

The railway pattern is a way of chaining operations where each might fail. It is called railway because there are two tracks in this pattern.

  • The first track is the happy path
  • The second track is the error track

If any of the chained functions fails we move to the error track. From there we get an error at the end of the railway.

For example, let say we want to:

  • Parse some external data
  • Validate the parsed data
  • Transform the data into something else
parseData : String -> Result String ParsedData

validateData : ParsedData -> Result String ValidData

transformData : ValidData -> Result String TransformedData

Pattern

In Elm this is commonly done using Maybe.andThen and Result.andThen. These function will run the next function in the chain if the previous function was successful, otherwise they will propagate the error.

process : String -> Result String TransformedData
process data =
	parseData data
		|> Result.andThen validateData
		|> Result.andThen transformData

Here is an excellent post about this with a lot more details.

Variant

A variant of this is where the second track doesn't represent an error, but rather an early exit.

E.g. This process finds recommendations for a user. Each function in the chain can add to the recommendations or choose to exit the process.

type Process
	= Continue Recommendation
	| Exit Recommendation

andThen : (Recommendation -> Process) -> Process -> Process
andThen callback process =
	case process of
		Continue document -> callback document
		Exit document -> Exit document

findRecommendations user =
	Continue emptyRecommendation
		|> andThen (findMusic user)
		|> andThen (findBooks user)
		|> andThen (findMovies user)
		|> andThen (findGames user)

findMusic : User -> Recommendation -> Process

...

andThen is a function that mirrors Result.andThen but specific for Process.

Pipeline builder

This is a common pattern used for decoders and validation. This pattern is used to build a function for processing some data using a series of piped functions.

type alias User =
	{ name: String
	, age: Int
	}

validateUser : User -> Result String User
validateUser user =
    Ok User
        |> validateName user.name
        |> validateAge user.age

This builds a function validateUser that will take a user and validate it. This validateUser function works like the railway pattern. We might get an Ok User at the end or an error Err String.

This pattern relies on the fact that a type alias in Elm can be used as a function. e.g. User is a function like:

String -> Int -> User

We start by putting the function (User) into a Result.

Then each function in the chain takes an attribute and the previous result, does the validation and returns a result back.

validateName : String -> Result String (String -> a) -> Result String a
validateName name =
    Result.andThen
        (\constructor ->
            if String.isEmpty name then
                Err "Invalid name"

            else
                Ok (constructor name)
        )

Complete example https://ellie-app.com/9SZTHJqB5r2a1

Caveat

When using this pattern we have to be careful with the order of functions in the pipeline. It is easy to make a mistake when the end type has many attribute of the same type.

type alias User =
	{ name: String
	, email: String
	}

With this type, we can mix up the order of name validation and email validation e.g.

    Ok User
        |> validateEmail user.email
        |> validateName user.name

This will work, but it will give us a User with the attributes mixed up:

{ name = "sam@sample.com"
, email = "Sam"
}

Some example packages using this:

Opaque types

Opaque types are types that cannot be created outside of a specific module. For example:

module Lib exposing (Config)

type Config = Config { size: Int, style: Style }

This module expose the Config type but not the constructor. An external module can't construct or modify a Config type.

Opaque types are useful for:

  • Enforcing invariants: Only the relevant module can change the data and ensure it follows some invariants.
  • Hiding the implementation to external modules. This is quite useful for building packages. Using opaque types makes it easier to change the implementation without breaking the code using it.

Anti-pattern

module Lib exposing (Config)

type alias Config = { size: Int, style: Style }

This module exposes Config transparently. Any changes we want to make will require changes in the module using this. In the case of a package, if we remove or change a type in Config we will have to publish a major version of this package.

Pattern

module Lib exposing (Config, newConfig, withSize)

type Config = Config { size: Int, style: Style }

newConfig : Config
newConfig =
	Config { size: 1, style : Big }

withSize : Int -> Config -> Config

This module allows an application to create a Config and update it. But if we decide to change how we store the Config we can do so without any breaking changes for the caller module using this.

The next page explains using opaque types for enforcing invariants.

Opaque types for enforcing invariants

Some times we want our data to always follow certain rules. E.g. We would like a list that is always sorted.

Using opaque types we can create a module that enforces this invariant.

module SortedList exposing (SortedList, new, add)

type SortedList comparable =
	SortedList (List comparable)

new : SortedList comparable
new =
	SortedList []

add : comparable -> SortedList comparable -> SortedList comparable

Only this module can create a SortedList as we don't expose the constructor.

Also, only this module can add an item to the list. By doing this we can enforce that the list is always sorted. External modules cannot change this data, so they are unable to break the sort invariant.

Combinators

Combinators is a technique where combining several values of the same type gives us back a value of the same type.

For example:

and : Filter -> Filter -> Filter

and is a function that takes two filters and combines them using an AND join. It gives us back another Filter. Given that the return value is the same type, we can keep combining them endlessly.

Some examples of combinators are:

  • Html
  • Cmd.batch
  • Parsers
  • JSON Decoder / Encoders
  • Filters e.g. (a AND (b OR c))
  • Validations

Anything that resembles a tree is a good candiate for using combinators.

Combinators allow us to:

  • Easily test each small part in isolation
  • Make complex systems from very small part
  • Create different combinations by cherry picking the parts we need

Phantom types

Phantom types are types that have a type variable on the type that is not used constructors. E.g.

type Users a =
	Users (List User)

This type variable allows us to restrict what type a function can take and return. For example:

type Active
	= Active

activeUsers : List User -> Users Active
activeUsers users =
	users
		|> List.filter isActive
		|> Users

activeUsers is a function that takes all users and only returns active users.

Phantom types are useful for things like:

  • Enforcing invariants in functions and views
  • Validation
  • State machines
  • Processes

In the example below we could have a view that only takes active users:

usersView : Users Active -> Html msg

The compiler will complain if we try to pass all users to this view. In this way it can be sure that we are filtering users correctly.

Process flow using phantom types

Sometimes we want to create a process that needs to follow different paths until it reaches an end point. Like a finite state machine.

For example we want an order form where you can change the total or the quantity. When you change any of these two, the other value needs to change automatically.

Form

  • When the user updates the total we want to update the quantity
  • When the user updates the quantity we want to update the total

These two flows can be illustrated with a state machine:

State machine

This is a simple example to illustrate this pattern. This particular example can be done with less ceremony, but in more complex scenarios this pattern is really valuable.

We want to design our code in way that:

  • Enforces running through the process steps in the correct order (depending on the user intention)
  • Doesn't let us forget a step

Anti-pattern

One possible way of doing this is by creating intermediate types for our process:


type alias InvalidOrder =
	{ quantity : Maybe Int, total : Maybe Int }

type alias OrderWithQuantity =
	{ quantity : Int, total: Maybe Int }

type alias OrderWithTotal =
	{ quantity : Maybe Int, total: Int }

type alias Order =
	{ quantity : Int, total : Int }

setTotal : Int -> InvalidOrder -> OrderWithTotal

adjustQuantityFromTotal : OrderWithTotal -> Order

setQuantity : Int -> InvalidOrder -> OrderWithQuantity

adjustTotalFromQuantity : OrderWithQuantity -> Order

Although this is not too bad here, if we were to have more paths and attributes the multiplication of intermediate types would get out of hand really quickly.

Pattern

Phantom types allow us to deal with this in an elegant way.

State machine states

First we need some types to define the state machine states:

type Step step
    = Step Order


type Start
    = Start


type OrderWithTotal
    = OrderWithTotal


type OrderWithQuantity
    = OrderWithQuantity


type Done
    = Done

The phantom type here is in Step. Note how it defines a step type variable that is not used in the constructor.

Transitions

Then we can create transition functions that use the phantom type to restrict what they take and return.

For example:

adjustQuantityFromTotal : Step OrderWithTotal -> Step Done

This function can only take a Step that is in the OrderWithTotal state. And returns a Step in Done state.

For our order state machine we need these functions:

start : Order -> Step Start

setTotal : Int -> Step Start -> Step OrderWithTotal

adjustQuantityFromTotal : Step OrderWithTotal -> Step Done

setQuantity : Int -> Step Start -> Step OrderWithQuantity

adjustTotalFromQuantity : Step OrderWithQuantity -> Step Done

done : Step Done -> Order

State machine

These functions are state machine transitions. They only allow moving from specific states to other specific states.

Flows

And finally we can build the valid flows using these functions:

flowPrioritizingTotal total order =
    start order
        |> setTotal total
        |> adjustQuantityFromTotal
        |> done


flowPrioritizingQuantity quantity order =
    start order
        |> setQuantity quantity
        |> adjustTotalFromQuantity
        |> done

In this way we can enforce specific processes without creating a myriad of intermediate types.

See a working example here https://ellie-app.com/smDDnCh5C8Xa1

Reusable views

It is common to need reusable views in our applications. The most basic way in Elm is to have view functions that take message constructors as arguments.

For example:

type alias Args msg =
	{ currentDate: Date
	, isOpen: Bool
	, onOpen: msg
	, onClose: msg
	, onSelectDate : Date -> msg
	}

calendar : Args msg -> Html msg
calendar args =

This view is easy to integrate and reuse. However it requires the caller to keep track of the state e.g. isOpen. Another limitation is that this view is incapable of producing commands.

Using the builder pattern

Reusable views like these are perfect candidates for the builder pattern:

Button.newArgs "Clear selection" Clear
	|> Button.withIcon IconClear
	|> Button.withSize Button.Wide
	|> Button.view

The nested Elm architecture

When an application starts growing large we might want to break the application messages into discrete parts. For example:

  • Root Application
    • Page 1
    • Page 2
    • ...

Pattern

The nested Elm architecture is a way of achieving this.

module Main exposing (main)

import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
import Sub


type alias Model =
    { count : Int
    , subModel : Sub.Model
    }


newModel : Model
newModel =
    { count = 0
    , subModel = Sub.newModel
    }


type Msg
    = Increment
    | Sub Sub.Msg


update : Msg -> Model -> Model
update msg model =
    case msg of
        Increment ->
            { model | count = model.count + 1 }

        Sub subMessage ->
            { model
                | subModel =
                    Sub.update subMessage model.subModel
            }


view : Model -> Html Msg
view model =
    div []
        [ div [] [ text <| String.fromInt model.count ]
        , button [ onClick Increment ] [ text "+1" ]
        , Sub.view model.subModel |> Html.map Sub
        ]


main : Program () Model Msg
main =
    Browser.sandbox
        { init = newModel
        , view = view
        , update = update
        }

Note the Sub.view model.subModel |> Html.map Sub in view.

Sub.elm :


module Sub exposing (..)

...

type alias Model =
    { count : Int }


newModel : Model
newModel =
    { count = 0
    }


type Msg
    = Increment


update : Msg -> Model -> Model
update msg model =
    case msg of
        Increment ->
            { model | count = model.count + 1 }


view : Model -> Html Msg
view model =
    div []
        [ div [] [ text <| String.fromInt model.count ]
        , button [ onClick Increment ] [ text "+1" ]
        ]

This pattern comes with its own set of challenges like:

  • Added boilerplate
  • It is not simple for the child module to communicate with the parent module (see Translator).

So this pattern is best used sparingly.

Child Outcome

As described in Nested TEA it is sometimes useful to create a nested Elm architecture. In that case we might want to send a message from the child to the parent after an action.

One simple way to do this is by returning a third value in the update function of the child.

module Child exposing
    ( Outcome(..)
    , update
    , ...
    )

type Outcome
  = OutcomeNone
  | OutcomeDateUpdated Date

update : Msg -> Model -> (Model, Cmd Msg, Outcome)

Then the parent module can deal with that:

module Parent exposing (...)

import Child

...

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
  case msg of
    ChildMsg childMsg ->
      let
        (nextChildModel, childCmd, childOutcome) =
          Child.update childMsg model.childModel

      -- Do something with the childOutcome

    ...

Translator

As described in Nested TEA it is sometimes useful to create a nested Elm architecture. When making this we normally would use Html.map to route messages back to the nested module.

For example, in the parent container you would have:

module Parent exposing(..)

import Child

Child.view model.childModel |> Html.map ChildMsg

The challenge of using Html.map here is that messages produced in the child module always need to route back to itself. We cannot easily produce a message in the child destined to its parent.

As an application grows it is common to encounter something like this, perhaps we need a UI element in the child module that needs to send the message to its parent.

Pattern

The solution to this is to make the child module views generic and provide a constructor for routing messages. So, instead of View Msg in the child module, it becomes View msg. Then the parent explicitly provides the constructor to route messages.

For example, we have a child module with a view like:

module Child exposing(..)

view: Model -> Html Msg
view model =
  div []
  [ button [ onClick Clicked ] [ text model.name ]
  ]

We want to add another button, but this time it should send a message to its parent.

First, make the child module generic

module Child exposing(..)

view: Model -> (Msg -> msg) -> Html msg
view model toSelf =
  div []
  [ button [ onClick (toSelf Clicked) ] [ text model.name ]
  ]

To toSelf is a constructor that wraps the internal message, producing a parent message. This replaces Html.map in the parent module and will route the message back to this module.

In the parent container we would use this view like:

module Parent exposing(..)

import Child

type Msg = ChildMsg Child.Msg

view model =
  ...
  Child.view model.childModel ChildMsg

Produce a parent message

With this setup we can produce parent messages from the child module.

module Child exposing(..)

type alias Args msg =
  { toSelf : Msg -> msg
  , onSave: msg
  }

view: Model -> Args msg -> Html msg
view model args =
  div []
  [ button [ onClick (args.toSelf Clicked) ] [ "Send to self" ]
  , button [ onClick args.onSave ] [ text "Send to parent" ]
  ]

The parent would call this like:

module Parent exposing(..)

import Child

type Msg
  = OnSave
  | ChildMsg Child.Msg

view model =
  ...
  Child.view
    model.childModel
    { toSelf = ChildMsg
    , onSave = OnSave
    }

Global actions

If we use the nested TEA for our application, we will most likely need a way for the nested modules to communicate with the top levels. For example:

  • Open a notification
  • Sign out the user
  • Return a value to the top level

There are many way of achieving this. One possible way is to have a module with global actions e.g.

module Actions exposing (..)

type Action
	= OpenSuccessNotification ...
	| OpenFailureNotifiation ...
	| ...

The all your nested module will return three elements on update. The third one being a list of actions to execute:

update : Msg -> Model -> (Model, Cmd Msg, List Action)

Any nested module in the chain could add an action to the list.

Finally your root update will need to map through the list and process the actions.


  • A nice pattern for adding actions is to mimic Cmd.batch. E.g. Actions.batch.
  • We might need to send a message back to the module that returned the action. E.g. Open a dialog with selections. In this case our actions might need a message associated with them e.g. Action Msg. We will need a Actions.map to just like Html.map.

The effects pattern

In a usual Elm application the update function returns (Model, Cmd Msg). Cmd in Elm is an opaque type so update functions are not easy to test. We cannot easily inspect the commands and see if they are doing the right thing. We cannot also simulate these commands as we don't know what they are doing.

Pattern

The effects pattern allows us to deal with these issues by returning an Effect type instead of Cmd msg from update.

type Effect
	= SaveUser User
	| LogoutUser
	| LoadData
	| ...

update : Msg -> Model -> (Model, List Effect)

At the last possible moment we convert the Effect into actual commands. E.g. in the root module of the app we would have a function like:

runEffects : List Effect -> Cmd Msg

This makes a Elm application a lot more testable. This is approach taken by elm-program-test.

Here is a more detailed blog post about this pattern.

Update return pipeline

Sometimes in the our update function we need to do many different things. For example:

  • Change the state of the model
  • Change some value in the browser query
  • Conditionally load more data
  • Do some analytics tracking

We can do all these things at once:

case msg of
	SeeReport report ->
		let
			nextModel =
				{ model
					| stage = ReportVisible report
					, loading =
						if needsToLoadMoreData model then
							Loading
						else
							model.loading

				}
			cmd =
				Cmd.batch
					[
					if needsToLoadMoreData model then
						loadMoreDataCmd
					else
						Cmd.none
					, setSomeValueInUrl
					, TrackEvent.track {... }
					]
		in
		(nextModel, cmd)

	... ->

But in these cases our update branches can get very complex very quickly. Making them difficult to understand and ripe for bugs.

Pattern

A nice way to make many things in an update branch is to break them by concerns and create a "return" pipeline.

case msg of
	SeeReport report ->
		(model, Cmd.none)
			|> andThen (setStageToReportVisible report)
			|> andThen loadMoreDataIfNeeded
			|> andThen addKeyInUrl
			|> andThen trackSeeReportEvent

In this case andThen is a function like:

andThen : (model -> (model, Cmd msg)) -> (model, Cmd msg) -> (model, Cmd msg)
andThen fn ( model, cmd ) =
    let
        ( nextModel, nextCmd ) =
            fn model
    in
    ( nextModel, Cmd.batch [ cmd, nextCmd ] )

This function takes another function that given the model returns a (model, Cmd msg) just like update. andThen takes care of batching commands together.

Every function is the pipeline will be responsible for only one thing, which is a lot easier to understand. E.g.

loadMoreDataIfNeeded : Model -> (Model, Cmd Msg)
loadMoreDataIfNeeded model =
	if needsToLoadMoreData model then
		({ model | loading = Loading }, loadMoreDataCmd)
	else
		(model, Cmd.none)

elm-return is a package that implements functions for this.