Elm's universal pattern

Joël Quenneville

In Elm, there are a lot of data structures that wrap other values. It’s very common to want to combine values inside these wrappers. It turns out that there’s a universal pattern in Elm to do this.

The setup

We have a user with a name, age, and address. The address is wrapped in its own type. Finally, our Model is a list of users.

type Address = Address String

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

To build a User from scratch, we could combine the various pieces like this:

user : User
user =
  User "Bob" 42 (Address "123 Main Street")

You could visualize this as:

Tree of the pieces that make up User

What if the various parts are wrapped in some type of container?

Maybe

Maybe allows us to tag values possibly not having a value. To wrap a value in a Maybe, you can use the Just constructor:

maybeName : Maybe String
maybeName =
  Just "Bob"

maybeAge : Maybe Int
maybeAge =
  Just 42

If we got the raw string for the address as Maybe String, how can we wrap it with our Address constructor while keeping it inside the Maybe? Enter Maybe.map:

rawAddress : Maybe String
rawAddress =
  Just "123 Main Street"

maybeAddress : Maybe Address
maybeAddress =
  Maybe.map Address rawAddress

Note that if rawAddress were Nothing then maybeAddress would be Nothing too.

What about combining our three Maybe values together to get a Maybe User ? Because the constructor User has three arguments and we are combining three Maybes, we can use Maybe.map3

maybeUser : Maybe User
maybeUser =
  Maybe.map3 User maybeName maybeAge maybeAddress

Once again, note that if any of maybeName, maybeAge, or maybeAddress is Nothing then maybeUser will also return Nothing.

The whole structure could be visualized as:

Tree of various Maybe components of a Maybe User

List

You are probably very familiar with how lists work. You can create them using the [] literal syntax.

nameList : List String
nameList =
 ["Alice", "Bob", "Carol"]

ageList : List Int
ageList =
  [25, 42, 64]

What about wrapping our Address type around a list of raw addresses? You probably correctly guessed we should use List.map.

rawAddresses : List String
rawAddresses =
  ["42 Some Plaza", "123 Main Street", "789 Central Avenue"]

addressList : List Address
addressList =
  List.map Address rawAddresses

Now how do we combine these three lists to get a list of users? If you said List.map3, you’re right!

userList : List User
userList =
  List.map3 User nameList ageList addressList

Combining multiple lists in this way, especially two lists into a list of tuples, is sometimes referred to as zipping.

The whole thing could be visualized as:

Tree of various List component of List User

You’re probably starting to see a pattern here. Let’s try something more difficult.

JSON decoders

Unlike the other wrapper types we’ve looked at so far, JSON decoders don’t wrap values in the traditional sense. Instead, they encode the relationship between a JSON structure and your Elm types.

Lets say we have the following JSON:

{ name: "Alice",
  age: "42"
  address: "123 Main Street"
}

You can tell Elm what type a value at a given key is using Decode.field and giving it a decoder. Json.Decode module provides decoders for the basic datatypes. For our name and age fields, it might look like:

nameDecoder : Decoder String
nameDecoder =
  Decode.field "name" Decode.string

ageDecoder : Decoder Int
ageDecoder =
  Decode.field "age" Decode.int

What if you want to wrap a decoded string with Address? There’s Decode.map for that.

rawStreetDecoder : Decoder String
rawStreetDecoder =
  Decode.field "address" Decode.string

addressDecoder : Decoder Address
addressDecoder =
  Decode.map Address rawStreetDecoder

We can combine all three decoders to decode a user with Decode.map3:

userDecoder : Decoder User
userDecoder =
  Decode.map3 User nameDecoder ageDecoder addressDecoder

You can visualize this as:

Tree of json decoders

Random generators

Like JSON decoders, random generators don’t wrap values in the traditional sense. Instead, they wrap the idea of a value that will be randomly generated in the future. This makes them a little bit harder to reason about but all the same rules apply as with the simpler data structures.

For simple values we can use the built-in generators:

ageGenerator : Generator Int
ageGenerator =
  Random.int 1 100 -- random number between 1 and 100

For the name, we want to pick from a particular list of strings. We can use the Random.uniform function to do that. It picks an item randomly from a list with equal probability.

nameGenerator : Generator String
nameGenerator =
  Random.uniform "Alice" ["Bob", "Carole"]

We can also use Random.map to wrap a random address string with Address:

rawStreetGenerator : Generator String
rawStreetGenerator =
  Random.uniform "42 Some Plaza" ["123 Main Street", "789 Central Avenue"]

addressGenerator : Generator Address
addressGenerator =
  Random.map Address rawStreetGenerator

Finally how can we combine all these generators to get a single generator of users?

userGenerator : Generator User
userGenerator =
  Random.map3 User nameGenerator ageGenerator addressGenerator

The combined generator can be visualized as:

Tree of random generators

Comparing side by side

You may have noticed a pattern as we were going through these four examples. Let’s look some of those functions side by side:

Building an Address

Maybe.map  Address rawAddress
List.map   Address rawAddresses
Decode.map Address rawStreetDecoder
Random.map Address rawStreetGenerator

Building a User

Maybe.map3  User maybeName     maybeAge     maybeAddress
List.map3   User nameList      ageList      addressList
Decode.map3 User nameDecoder   ageDecoder   addressDecoder
Random.map3 User nameGenerator ageGenerator addressGenerator

General Concepts

You’re probably seeing a pattern now. Here are some general tips for doing this type of work:

  • When building complex structures, start with the smallest sub-part of your structure (generally a primitive value) and then combine them with each other to form more complex structures.
  • You can keep transforming and combining those combined structures as much as you want to create arbitrarily complex structures.
  • Use map to transform or wrap a value inside a wrapper structure.
  • Use map2, map3, and so on to combine multiple wrapped values together
  • If you get into a situation where mapping gives you nested containers (e.g. Maybe (Maybe Address)), you’ll want to take a look at the andThen function for your container.