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.