You can find the code for this blog post at bahmutov/if-monad-frp.
First, I will show an If/Else refactored into two implementations of
Either
monad. Then I will show how to implement condition using
reactive streams.
Goal
Given a simply predicate (true or false), execute first or second function.
We traditionally write imperative conditions using JavaScript built-in
if-else
operator
1 | const condition = true |
While this code is simple, it will quickly become hard to read and update when the condition becomes more complex, and the number of statements in "true" and "false" branches grows large. What can we do to combat the future complexity?
Imperative refactoring
We can separate the condition check from the code inside the "true" and
"false" branches. For example in if.js I have split the if - else
code from the actual callback
functions.
1 | const condition = true |
Good, but the condition and the branches are still tied together. It is hard
to add more statements to functions f
or g
if we want to execute
more code depending on the condition. Enter a wrapper around the evaluated
condition - the Either
monad.
Folktale Either monad
First, let us take Either
monad from the large Folktale
library. It wraps around the evaluated condition and allows us to specify
functions to be executed later.
See the code in either.js that creates either
(get it?) Either.Right
object if the predicate is true or Either.Left
object if the predicate is false.
1 | const Either = require('data.either') |
We only created an anonymous Either
object, but how do we actually execute
our branches? We tell the Either
instance to execute our two callbacks
by passing them to .fold()
method call.
1 | truthyEither(condition) |
Notice that the "false" branch function g
is at the first position (that
is why it is called "Left"), while the "true" branch function f
is the second.
1 | .fold(falseBranchFunction, trueBranchFunction) |
Also notice, that the condition has already been evaluated, and while we pass
two functions to .fold(g, f)
, the Monad will only evaluate one of them.
What if we want to run more functions in the "true" branch, which is a
common requirement when things change.
We can easily pass an additional function as a callback using .map()
method.
1 | // run more functions in "true" branch |
Do we need the condition constructor function?
1 | function truthyEither(c) { |
Unfortunately we need this function.
The library only comes with a shortcut for creating
either Right
or Left
based on the argument being "null" or "undefined",
not "truthy" or "falsy".
1 | var o = ... // could be undefined or null |
Ramda Fantasy Either monad
Another functional programming library Ramda Fantasy takes a different approach to "extracting" values from the condition "black box" and running the success and failure branches.
We construct the monad the same way, see fantasy-either.js
1 | const Either = require('ramda-fantasy').Either |
But to actually execute "true" and "false" branches we need to use the static
function Either.either
.
1 | Either.either(g, f, truthyEither(condition)) |
Notice that we passed the "left" function first, then the "right" one, and then the monad instance itself. Why is it this way?
The Either.either
function, just like all functions in
Ramda library are curried, and with the order of
arguments strongly in favor of "data last". This is because we often know
the code to execute (our "true" and "false" branch callbacks) before
we get the condition monad. With curried Either.either
we can write the
code like this, preparing what to do first and then waiting for data.
1 | const whatToDo = Either.either(g, f) |
Functional reactive programming
Everything changes when we try writing a program that models data flow between event sources and sinks. Instead of handling a single condition and executing "true" or "false" paths once, we must setup "pipelines" that can execute multiple times.
To show this practice I will use a small reactive library xstream. Since it a module transpiled from TypeScript to ES6, I will use babel-cli module to actually execute it. You can find the code in frp.js file.
1 | npm i -D babel-cli babel-preset-es2015 |
First, we have same setup as before
1 | import xs from 'xstream' |
Instead of a single condition code, we have a stream of predicate events. For example, we could have 3 events
1 | const s$ = xs.of(true, false, true) |
Here is the most important difference between regular function programming
(rFP) and functional reactive programming (FRP): an if-else
branch becomes
a fork in the events pipeline. One output "pipe" (or stream) can have
"truthy" events, while the other can have "falsy" events. In fact, it does not
have to be binary - you can have multiple "filters" and direct the event
into one of the multiple output pathways, or even "clone" same event into multiple
output pathways.
For clarity I will create named streams for "truthy" events and for
"falsy" events. In our case they are disjoint, and an event is passed into
one of these stream depending on the predicate function passed to
.filter
method.
1 | const truthy$ = s$.filter(c => c) |
Now we just need to start the events flowing, which we can do by adding listeners to the above two streams.
1 | truthy$.addListener({ |
1 | $(npm bin)/babel-node frp.js |
Note that the "truthy" stream runs immediately, because all the predicate
events are available immediately due to xs.of(true, false, true)
.
In the real world situation, the events are usually generated via async
events, thus the order in which the streams truthy$
and falsy$
execute are closer to the input events order.
- Check out JavaScript Journey - repo with the same small example implemented in different programming styles.
- Read THE MARVELLOUSLY MYSTERIOUS JAVASCRIPT MAYBE MONAD blog post - the
Maybe
monad is very close toEither
monad described in this blog post. - 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 and reactive blog posts.