The Meaning of 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. One of my favourite features of Elm is its type system. It’s taken me some time to get comfortable with Elm’s types, but the more I get to know them the more I love them.

Alongside learning Elm I’ve been reading through the excellent Haskell Book, and it’s definitely given me a deeper understanding about using types that I’d like to share, particularly about using map - (Don’t worry there won’t be any crazy category theory speak).

Map in JavaScript

A lot of people coming to Elm will have had exposure to JavaScript, and as JavaScript programmers our main exposure to map is through Arrays.

map let’s you apply a function to all the items in an array, it’s simple and incredibly useful. If you’ve read many articles on functional programming in JavaScript you will have seen the example below a nauseating amount of times!

> [ 1, 2, 3, 4, 5 ].map(x => x * 2)
> [ 2, 4, 6, 8, 10 ]

Map is great ❤️.

From an Array to a List

A List in Elm then seems like the most obvious comparison to a JS array, it’s a bunch of values with the same literal brackets [] and you can apply a function to each of them with List.map. If you’ve used map in JS, the example below should look reasonably similar:

> 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 on earth 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

Imagine for a second that the square brackets of the List are walls. Imagine also that our double function \x -> x * 2 has stubby little legs! Try as it might the poor thing can’t get over the wall!

We use map to “lift” the function over the wall of the List to reach the values inside it. The wall is still intact after but the values are now changed. map gave our double function wings!

don’t be phased by the wall

Just get lifted

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 all fine and well but what if we want to change the value inside the Just (how about doubling it, we’ve got a function that does that already right!) but preserve the fact that the integer might not be there? We might just pull 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 gets kinda long writing out those case expressions if we have to do it lots of times. It turns out there’s a handy little pattern for this, and it’s called map! This is Maybe.map from the Elm Core source code, looks kinda similar to our maybeDouble 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 our ordinary, stubby legged 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 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. — Haskell Book

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 an Elm date instead rather than having to faff around with it later in other bits of our application that need it as an Elm date. Json.map to the rescue!

import Date exposing (fromTime)
import Json.Decode as Json

-- Json.int : Decoder Int
dateDecoder : Decoder Date
dateDecoder =
Json.map fromTime Json.int

-- or in pipeline style
dateDecoderPipe : Decoder Date
dateDecoderPipe =
Json.int |> Json.map fromTime

Great! We can now use our dateDecoder instead of the int decoder to turn the server data directly into an Elm date.

A Decoder may seem a little more abstract than something like a Maybe (at least I still find it a little abstract). But just remember, take a step back, look at the type signature:

Decoder Int

The Decoder is the structure and the Int is the value. We can lift a plain old function over the Decoder wall and change the Int inside it.

Why bother?

You may be thinking, this seems like a lot of fanfare about a simple function. However, I think understanding map in this way helps us get to grips with some of the reasons why Elm is such a powerful language.

Elm 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 the power to describe our data with this extra level of richness. Using types this way brings so much reality and truth to our data models; we have to face the hard truths of our data up front, but it gives us the power to handle all sorts of messy and weird situations gracefully and robustly.

The humble map is one of the tools that makes working with and preserving these rich contexts easier. We can reuse plain functions in all sorts of different contexts! Now go forth and lift your functions to a higher state of consciousness! 🌀😎🌻

Next Time

We’ll have a look at some structures that have more than one embedded value (Like Result), using map2, map3 and even the mysterious andThen 😱.

Some Random Final bits

If you’ve understood most of this article, you know what a Functor is! Congratulations you category theory whizz!

In Haskell, the equivalent of Elm’s versions of map is called flatmap, and any value of a type that implements flatmap properly 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.”
George Clinton, Pioneer of funkiness
Show your support

Clapping shows how much you appreciated Andrew MacMurray’s story.