The intuition behind applicative

April 11, 2017 0 Comments

The intuition behind applicative

 

 

Why would you want a function in a box?

JavaScript functions are pretty simple. Consider unary function toUpper below

Given a string, it returns the uppercase variant.

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

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

Nice!

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

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

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

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.

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

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

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

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

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

Wrap all the things

So far we have done the following

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.

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!).

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

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

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)


Tag cloud