Map in Elm
Understanding and using Elm’s many maps
TL;DR; map is not just for lists, you can use map to change the values inside structures like Maybe, Result and even Decoder whilst preserving that structure.
I’ve been enjoying writing Elm a lot recently; my favourite feature by far is its type system. It’s taken time to get comfortable with types, but the more I use them the more I love them.
Alongside Elm I’ve been working through the excellent Haskell Programming from First Principles. It’s definitely given me a deeper understanding of the usefulness of types that I’d like to share, particularly about certain types and their relationship with map - (Don’t worry there won’t be any category theory speak).
Map in JavaScript
Many coming to Elm will have used JavaScript, where map
is commonly seen as a method on Array
. map
applies a function to all items in an array, it’s simple and very useful. There are countless examples like the one bellow in articles on Functional programming in JavaScript.
> [ 1, 2, 3, 4, 5 ].map(x => x * 2)> [ 2, 4, 6, 8, 10 ]
From Array to List
List
in Elm is a close relative to JavaScript’s Array
, it’s a collection of values (surrounded by the same literal brackets []
) and a function can be applied to each item with the List.map
function. If you’ve used JS map, the example below should look reasonably familiar:
> List.map (\x -> x * 2) [ 1, 2, 3, 4, 5 ]> [ 2, 4, 6, 8, 10 ] :: List number
But what about Maybe.map
or Result.map
? And what do List.map2
and List.map3
do? What does it even mean to “map” over a Result
or a Maybe
and why would we want to do this?
Over the Wall
Picture for a moment that the square brackets of the List are walls. Imagine also that the function we want to apply \x -> x * 2
can’t reach over the wall!
We use map
to “lift” the function over the wall of the List to reach the values inside. The wall is still intact after but the values are now changed.
Maybe.map
Now say that instead of a List we’ve been given a value which is a Maybe Int
. It could be Just
an integer or it might be Nothing
. That’s fine but what if we want to change the value inside the Just
, but hang onto the Maybe
structure (the information that the Int
might not be there is useful to us). We could start by pulling it apart with a case expression:
If we get Just
a value, double the value inside the Just
and return it wrapped in another Just
, and if we get Nothing
just return the Nothing
:
maybeDouble : Maybe Int
maybeDouble =
case maybeValue of
Just value ->
Just (double value) Nothing ->
Nothing
maybeValue : Maybe Int
maybeValue =
Just 3
double : number -> number
double x =
x * 2
This is fine if we only have to do it once but becomes long and tedious writing case expressions if we have to do it lots of times. Surely this pattern can be abstracted out? It turns out this pattern has a name, and it’s called map! This is Maybe.map
from the Elm Core source code, looks similar to the maybeDouble
example above right?
https://github.com/elm-lang/core/blob/master/src/Maybe.elm#L56-L65
map : (a -> b) -> Maybe a -> Maybe b
map f maybe =
case maybe of
Just value ->
Just (f value) Nothing ->
Nothing
And here’s how we’d use it to get our maybeDouble value:
maybeDouble : Maybe Int
maybeDouble =
Maybe.map double maybeValue
Maybe.map
takes the double function and “lifts” it over the wall of the Maybe
so it can reach the value inside. The difference here is that if you give Maybe.map
Nothing
it just returns it as is. Imagine similarly if you gave List.map
an empty list, it would just return the empty list unchanged. Starting to see the pattern?
Rethinking what map is
Stepping back from a List we can now think of map as:
a way to apply a function over or around some structure that we don’t want to alter.
That is, we want to apply the function to the value that is “inside” some structure and leave the structure alone. — J.Moronuki, C.Allen (Haskell Programming ffp)
List
, Maybe
, Result
, Task
, Decoder
are all pieces of structure that hold onto values, we can use map on any of them to reach inside and change those values.
Decoded
For a more realistic example, say we’re getting some JSON data back from a server: one of the fields is a date as the number of milliseconds since Jan 1 1970. It would be nice if we could turn this into a Posix
value rather than change it later in multiple bits of our application. Json.Decode.map
to the rescue!
import Time
import Json.Decode as Json
-- Json.int : Decoder Int
timeDecoder : Decoder Time.Posix
teimDecoder =
Json.map Time.millisToPosix Json.int-- or in pipeline style
dateDecoderPipe : Decoder Time.Posix
dateDecoderPipe =
Json.int |> Json.map Time.millisToPosix
Great! We can now use our timeDecoder
instead of the int
decoder to turn the server data directly into a posix value.
A Decoder
may seem a little more abstract than something like a Maybe
(I still find it a little abstract). But just remember, take a step back and look at the type signature:
Decoder Int
The Decoder
is the structure and the Int
is the value. We can lift an ordinary function over the Decoder
wall and change the Int
inside it.
Why bother?
You may be thinking, this is a lot of fanfare about a simple function. However, I think understanding map in this way helps us better understand why Elm’s type system is so powerful.
Using types in this way reminds us that a lot of our data — if not nearly all of it — lives in a context. Just to name a few:
- The data might be a collection of values
- Values might not be there
- Computations might screw up if they’re given a bad value
- Web requests might fail for a bunch of reasons
Data always comes from somewhere.
Elm gives us neat syntax and power to describe data with an extra level of richness. Using types this way models the world we’re trying to represent in software more accurately; we have to face the hard truths of our data up front, but it gives us power to handle all sorts of messy and weird situations gracefully.
The humble map
is one of the tools that makes working with and preserving these rich contexts easier. We can reuse our plain functions in many different contexts. Now go forth and lift your functions to a higher state of consciousness! 🌀😎🌻
What about map2, map3 etc?
The 2s, 3s and onwards are useful when you have multiple values embedded in a structure and a function that needs access to the values inside those structures. Say we have 2 Maybe Int
s and we want to add them both together — we could write it like this:
add : Int -> Int -> Int
add a b =
a + b
maybeAdded : Maybe Int
maybeAdded =
Maybe.map2 add (Just 1) (Just 3)-- Or with (+) function
maybeAddedWithInfix : Maybe Int
maybeAddedWithInfix =
Maybe.map2 (+) (Just 1) (Just 3)
This returns Just 4
. If either of the values were Nothing
the result would be Nothing
(all without the headache of pattern matching each maybe). The 2s, 3s, 4s correspond to how many values (e.g. Maybes) you have and the number of arguments the function being applied takes.
Some Random Final bits
If you’ve understood most of this article, you know what a Functor is! Congratulations category theory whizz!
In Haskell, the equivalent of Elm’s versions of map is called fmap
(functor map), and any value of a type with a correct implementation of fmap
is a Functor
.
The authors of Haskell Book love George Clinton, “one of the most important innovators of funk music”, and the album “Mothership Connection”:
“You can pretend the album is mapping your consciousness from the real world into the category of funkiness if that helps.”