Imagine we have a small piece of code that, given a number multiplies it by 5 and adds 3 to the result. We can write this in the imperative style using two tiny helper functions
1 | // compute 2 * 5 + 3 |
The expression console.log(add3(mul5(2)))
is a nested call of 3 functions:
first the mul5
is called, followed by add3
,
finally followed by console.log
. In each case,
the result of the inner function call is passed to the outer function.
We can see this clearly by being more explicit in our code
1 | const initial = 2 |
As a personal preference, I prefer reading functional calls top to bottom to reading them inside out (equivalent to right to left).
The combined expression console.log(add3(mul5(2)))
is called "composition"
- the nested function calls are composed together. The later multi-line code is not a composition, and has a disadvantage - the variables are all shared in the same namespace, making the code brittle. Accidental variable overwrite is always dangerous.
Can we combine the advantage of the compositional approach (no stray variables) with the clarity of the imperative approach? Yes, by wrapping the common task of the multi-line imperative approach in a piece of utility code - a simple "box" functor.
Notice the common pattern in each line. The imperative code does several things over and over:
- receives the result of a function call with a single argument
- stores the result in a variable
- calls next unary function passing the variable as an argument
- goes to step 1
In JavaScript the parts we want to automate look like these (commenting out everything else)
1 | const privateVariable = ... |
Box functor
Here is a simple utility function that hides a given value
to avoid accidental name clashes.
The returned "box" automates the above
"call function - store result - call next function" algorithm.
The only method the returned "box" object has is .map
. The map
is used to execute a given unary function.
1 | const Box = _ => ({ |
Note the result is stored "back" in a returned "box", thus the process can
continue. The composition is thus replaced with calling the box's
.map
function repeatedly.
1 | Box(2) |
Hmm, isn't the above "Box" an example of Object Oriented Programming (OOP)? No. We capture the private variable using closure, and we never use "this" or "new" keywords.
If we need to grab the synchronous value from the "box", we can add a function
to return the value from the closure, avoiding the awkward
.map(console.log)
call. This allows us to connect the functor "box" to the
rest of the imperative code.
1 | const Box = _ => ({ |
Note that box.fold()
call is a terminal method - one cannot attach
any more .map()
calls after the .fold()
, because the wrapped value
is returned. We can still continue mapping over the original "box" though.
1 | const box = Box(2) |
Sometimes it is useful to just print the value for debugging,
which we can do by adding .inspect
method to the "box"
1 | const Box = _ => ({ |
Maybe functor and composability
The "box" just implements simple common imperative logic
1 | const privateVariable = ... |
One can place more logic inside the .map()
method, for example, we can
put the "truthy" condition on the value inside, creating "maybe" functor.
1 | const privateVariable = ... |
What happens if the condition is false? Just like in a regular if/else
tree we skip all the branches. Look at the binary tree formed by nested
"success" paths
1 | if (condition1) { |
In all cases, the failure is the same, and should skip any other evaluation.
To implement we need special "placeholder" type we
can return that will skip all subsequent .map(fn)
calls. In my code let
us call it "None".
1 | const None = () => ({ |
Note that we can transition from Maybe
to None
but not in the opposite
direction. Once the condition is false, everything else is skipped.
The above code makes it simple to write code that is safe against undefined or null values.
1 | console.log( |
The above code is also composable. Ordinarily, we cannot execute more
statements inside "if/else" branches. For example, if we execute the given
function "foo" where f
and g
are functions, and want to execute
function h
as well, we have to call the condition again outside foo
ourselves.
1 | function foo(value) { |
Using Maybe
, we can just return the functor and compose call to
function h
by adding .map(h)
to the returned functor!
1 | function foo(value) { |
That is why functors are wrappers that allow us to compose functions.
- Watch the video course Professor Frisby Introduces Composable Functional JavaScript to learn all aspects of FP in JavaScript. Or read the book
- Read my other functional blog posts.
Update 1
A sharp-eyed reader Irakli Safareli
@safareli has noted that the above
functor Maybe
is not really a functor because it does not respect the
following composition law (that every Functor should obey)
1 | // for any initial value and functions f and g |
For example in this case the results are different
1 | const g = _ => null |
Hmm, what is going on? The problems is that f(g(x))
expression
does not follow what we implemented in the above Maybe
.
The above Maybe
tries to mimic the success path were every input
argument is checked before calling the next function, and not just the initial
value.
1 | const first = ... |
In our case the predicate evaluation happens on every .map()
, but the
Functor should NOT make such decisions if it wants to just compose functions.
This would be equivalent to making a decision only when constructing the
functor instance initially.
1 | const first = ... |
If we really wanted to implement the conditional logic at each step,
functions f
and g
could return an instance of Maybe (that implements
the predicate logic at creation).
Of course then we should implement the additional logic to deal
with functions that return wrapped results. And those would be Monads
with their .chain
method :)