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 | let someVar // value maybe a string or not |
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 | const Box = x => ({ |
Nice!
We can keep mapping over the value in the box over and over if necessary, since map
returns
same type of box.
1 | const toUpper = s => s.toUpperCase() |
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 | const Maybe = x => ({ |
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 | toUpper(someVar) |
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 | let f = toUpper |
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 | const name = f => f.name |
The above use again keeps the argument value in the Maybe
box, equivalent to wrapping
the x
in the expression f(x)
1 | Maybe(function foo() {}) |
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 | const Box = f => ({ |
Interesting, instead of wrapping argument x
in the expression f(x)
we wrapped the function
variable f
.
1 | Box(toUpper).ap('foo') |
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 | f(x) // applies original function "f" to original value "x", returns simple 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 | const Box = value => ({ |
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 | const add = (a, b) => a + b |
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 | const add = a => b => a + b |
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 | const add = (a, b) => a + b |
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)