Map in Elm

Understanding and using Elm’s many maps

Andrew MacMurray
6 min readJul 12, 2017

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.

don’t be phased by the wall

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 Ints 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.”

George Clinton, Pioneer of funkiness

--

--