The intuition behind applicative

Why would you want a function in a box?

JavaScript functions are pretty simple. Consider unary function toUpper below

1
const toUpper = s => s.toUpperCase()

Given a string, it returns the uppercase variant.

1
toUpper('foo') // 'FOO'

Yet the function is unsafe. If we pass a non-string value, the entire world crashes

1
2
let someVar // value maybe a string or not
toUpper(someVar) // 🔥

We need to protect against invalid values when calling toUpper, and we can do this by noticing that we really apply a mapping when we call toUpper. The mapping takes a string and returns another string. Thus it makes sense to put the argument into a "box" and call toUpper on the value using some kind of map method. For example

1
2
3
4
5
const Box = x => ({
map: f => Box(f(x)),
toString: _ => `Box(${x})`
})
Box('foo').map(toUpper).toString() // Box(FOO)

Nice!

We can keep mapping over the value in the box over and over if necessary, since map returns same type of box.

1
2
3
4
5
6
const toUpper = s => s.toUpperCase()
const toLower = s => s.toLowerCase()
Box('Foo')
.map(toUpper)
.map(toLower)
.toString() // Box('foo')

We can protect against invalid values in the box, for example by creating a box that checks its argument at creation and does NOT run map if the value inside is invalid. Please ignore all shortcuts in this example, this is just to show the guard condition

1
2
3
4
5
6
7
8
9
10
const Maybe = x => ({
map: f => typeof x === 'undefined' ? Maybe() : Maybe(f(x)),
toString: _ => `Maybe(${x})`
})
Maybe('foo')
.map(toUpper)
.toString() // Maybe('FOO')
Maybe()
.map(toUpper)
.toString() // Maybe(undefined)

Most importantly, there is NO crash when we use Maybe().map(toUpper) because we have not called toUpper with invalid input!

Symmetry

Notice what we have done to the original code we wanted to protect

1
2
3
4
toUpper(someVar)
// -------
// ^
// protected using Maybe

It is a function call statement f(x) and it needs two things: a function and an argument. Since functions are first class citizen, there is nothing stopping us from storing either one as a variable or even both.

1
2
3
let f = toUpper
let x = 'foo'
f(x)

If we can wrap the x variable, why can't we wrap the variable f? There is no reason! We could wrap f in the same box and pass it safely to another function. For example, if we wanted to get the name of a function, we could write

1
2
3
4
const name = f => f.name
Maybe(function foo() {})
.map(name)
.toString() // Maybe('foo')

The above use again keeps the argument value in the Maybe box, equivalent to wrapping the x in the expression f(x)

1
2
3
4
5
6
7
Maybe(function foo() {})
.map(name)
// same as
name(foo)
// ---
// ^
// value in the Maybe box

But what if we wrap the left side - the function reference - and then pass argument to it? Again, please ignore all the data type laws, this is just an example

1
2
3
4
5
6
const Box = f => ({
ap: x => Box(f(x)),
toString: _ => `Box(${f})`
})
const toUpper = s => s.toUpperCase()
Box(toUpper).ap('foo').toString() // Box('FOO')

Interesting, instead of wrapping argument x in the expression f(x) we wrapped the function variable f.

1
2
3
4
5
6
7
8
Box(toUpper).ap('foo')
/*
same as
toUpper('foo')
-------
^
function is the wrapped value
*/

We picked the method name ap because it is short for apply which is standard JavaScript operator on functions

1
toUpper.apply(null, ['foo']) // 'FOO'

Wrap all the things

So far we have done the following

1
2
3
f(x)          // applies original function "f" to original value "x", returns simple value
Box(x).map(f) // applies original function "f" to wrapped value "x", returns wrapped value
Box(f).ap(x) // applies wrapped function "f" to original value "x", returns wrapped value

You know what is still missing for complete symmetry? If we can wrap just one of the two variables in the function execution statement, why not both?

The simplest way to do this is to pass a wrapped value to .ap() method so it looks like this Box(f).ap(Box(x)). This is simple to implement if we already have method .map(f) available in our wrapper object. For simplicity I will call whatever is inside Box generic value because it could be a function or not.

1
2
3
4
5
6
const Box = value => ({
map: f => Box(f(value)),
ap: x => x.map(value), // see the .map() trick here? value must be a function!
toString: _ => `Box(${value})`
})
Box(toUpper).ap(Box('foo')).toString() // Box('FOO')

Our original simple code f(x) // returns result now has all parts wrapped in the same Box object: Box(f).ap(Box(x)) // returns Box(result).

Bonus: curried functions

A JavaScript function returns a single result (generators and observables aside). If we wrap in a Box a binary function, like function add(a, b) { return a + b }, we cannot call x.map(value) on it, because our map passed a single value (our Box only can keep one thing at a time!).

1
2
3
const add = (a, b) => a + b
Box(2).map(add) // NaN
// just like add(2) returns NaN

Somehow our function add has to accept a single function and wait for the second argument. In functional programming providing a single argument to a function to make another function that expects the second argument is called partial application (hmm, ap- prefix again), and functions that can wait for a single argument at a time automatically are called curried.

The simplest way to make a binary function into a series of unary functions is by using fat arrows :) As a habit I now write addition like this const add = a => b => a + b and this form works really well with our Box wrapper

1
2
3
4
const add = a => b => a + b
Box(add).ap(Box(2)).ap(Box(3)).toString() // Box(5)
// same as
// add(2)(3) // 5

If you really do not want to change the original add function into curried form, you can curry it "in place" when creating Box wrapper like this

1
2
const add = (a, b) => a + b
Box(a => b => add(a, b)).ap(Box(2)).ap(Box(3)).toString() // Box(5)

Final thoughts

  • If you have a plain value, it can be wrapped and mapped over a function Box(x).map(f)
  • If you have a function, you can wrap it and apply to a wrapped value Box(f).ap(Box(x))
  • You can apply wrapped function that expects multiple arguments if you curry the function

You should also watch / read these resources (and they are much better than my blog post anyway)