Typically when we think of map we think of lists, but we can widen our thinking of map and through it get a simpler understanding of how the elusive JavaScript Promise works.

1 the type signature of map

Let’s take the ML notation for map of a list:

((a -> b) -> List a -> List b)

With ML notation, the best way to read this is that the last arrow is the return type. The reason the notation exists this way is because functions in a functional language can be modeled as unary, or meaning they only have one argument. One can imagine multiple argument functions as sytactic sugar.

Take this add example:

const add = x => y => x + y

The ML notation for add is (number -> number -> number), where the first two numbers are parameters and the third is the return type.

We’ll use this add function later to show off map in action, since map needs a function to apply to some data.

Embedded functions are delineated with parenthesis. Looking back at our map example:

((a -> b) -> List a -> List b)

(a -> b) means the first argument is a function that takes an a and returns a b. In ML’s type notation, type parameters can appear as lowercase letters. It’s also notable that type parameters such as a and b can be the same type, but they are permitted to differ. Depending on how deep you get into type theory, a and b could represent any different form of data. For example the type of a could be the number 5 (not just any number), and type b would be something related to 5 by virtue of the transformation function.

In functional programming, usually the arguments that “configure” or specialize the function come first. The data portion of the function comes last. If you come from a background such as Ruby, C#, Java, or JavaScript (think lodash), this will seem backwards to you. The reason this is done is to aid with partial application - an important tool in function composition.

2 expressing map functionally

We could write our list’s map function like this:

const mapList = f => xs => xs.map(f)

Yes, we’re borrowing from the existing Array.prototype.map here. We will come back to this function later.

Back to map: Let’s take a step back from our map signature. Should we care about whether or not it’s a list we want to transform? Could we apply map in other contexts? Some languages allow map to operate on a map structure (an associative list), where each value undergoes a transformation and the operation produces a new map structure.

Lists can be thought of in a more generic sense: a Functor. In the vaguest sense a Functor is a kind of structure. Even calling a Functor a “container” might make some intuitive sense, but that can be too constraining a term.

Functors are generally written as F a, meaning a Functor parameterized by type a. Functors themselves are type parameters, which are further parameterized by a. This is because there isn’t a hierarchy of class inheritance in this model. Instead a Functor is a loose category of things that you can apply map to. The language feature to have type parameters on a type parameter is called Higher Kinded Types, or HKT. Without it, we must express these types individually. So that means one map type for lists, one for associative lists, and so on.

With the Functor, the ML expression for map becomes:

((a -> b) -> F a -> F b)

3 promise

Tiny! Now let’s take a look at something more complicated in JS: Promises. Stay with me while we stitch this together. Promise in JS has a then method that can be used to operate on the promises’ data. You could think of it as a callback that is executed when the promise resolves, but that’s very imperative thinking and we don’t do that here. For a moment, let’s make a functional-esque version of Promise's then:

const then = f => p => p.then(f)

This looks familiar, but the names are different. This is essentially the same thing as map. Let’s do some renaming and create a real method for Promise: map.

Promise.prototype.map = function(fn) { return this.then(fn) }

Basically we made an alias here. There’s no material difference between map and then. The map method just delegates to then. Let’s go back to our list version of map and rewrite it to account for functors:

const map = fn => f => f.map(fn)

Now map works for both Array and Promise. One of the things we get from doing this is we create an ecosystem that begs for function composition. With function composition we’re just stitching together functionality from things we already understand because they are very simple.

4 bonus: maybe

Even if we don’t go deep into a rich ecosystem of curried functions, we can still benefit having these two things nudged a little closer. Take our add function we wrote earlier, and let’s compose it with map.

One may think of Maybe as simply being a sum type of a | Nothing. In JavaScript you could think of this as anything | void (where void is null or undefined, thanks JS!). This gets messy if that anything is null. More importantly neither of these things necessarily have map, and we can’t easily tell which of the two members in the union we are looking at.

This is why Maybe is expressed as Just a | Nothing. Just a has map, which allows us to transform a without really knowing if we are dealing with a Just a or a Nothing. Similarly Nothing has map, but it’s a no-op since there is nothing to do. Get it?

new Just(1)    // Just is the "value" form of Maybe.
  .map(add(1)) // Just(2)
  .map(add(2)) // Just(4)

new Nothing()  // Nothing is the "null" form of Maybe.
  .map(add(1)) // Nothing
  .map(add(2)) // Yep. Still Nothing.

It’s important to note in both cases we are dealing with a Maybe and therefore we are safe to pass a Maybe number around.

5 bonus bonus: either

Either is similar to Maybe. Its type is Left a | Right b. Left is the no-op case and Right is the case we care about, at least in regards to map.

new Right(1)   // Right is the "value" form of Either, by convention.
  .map(add(1)) // Right(2)
  .map(add(2)) // Right(3)

new Left(1)    // Left is the "left" form of Either.
  .map(add(1)) // Left(1)
  .map(add(2)) // Left(1)

6 in conclusion

I hope this has been informative of the power of map. When you hear FP enthusiasts talking about how most everything can be handled with some combination of map, filter, and fold (reduce), one can see how it’s more than just list comprehensions which few of us get to remain in when writing real world software.