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:
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
Maybe
s, 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:
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:
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:
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:
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 theandThen
function for your container.