If Else vs Either Monad vs FRP

Imperative If/Else example implemented using Either monads and reactive style.

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
2
3
4
5
6
const condition = true
if (condition) {
console.log('it is true')
} else {
console.log('nope')
}

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
2
3
4
5
6
7
8
9
const condition = true
const f = () => console.log('it is true')
const g = () => console.log('nope')
// now execute the condition and one of the functions
if (condition) {
f()
} else {
g()
}

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
2
3
4
5
6
7
8
9
10
11
const Either = require('data.either')

const f = () => console.log('it is true')
const g = () => console.log('nope')
const condition = true

function truthyEither(c) {
return c ? Either.Right() : Either.Left()
}

truthyEither(condition)

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
2
3
truthyEither(condition)
.fold(g, f)
// it is true

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
2
3
4
5
6
7
8
// run more functions in "true" branch
const h = () => console.log('BREAKING NEWS!')
truthyEither(condition)
.map(h)
// maybe even more .map calls!
.fold(g, f)
// BREAKING NEWS!
// it is true

Do we need the condition constructor function?

1
2
3
function truthyEither(c) {
return c ? Either.Right() : Either.Left()
}

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
2
3
var o = ... // could be undefined or null
Either.fromNullable(o)
.fold(invalidO, goodO)

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
2
3
4
5
6
7
8
9
10
const Either = require('ramda-fantasy').Either

const f = () => console.log('it is true')
const g = () => console.log('nope')
const condition = true

// separate condition
function truthyEither(c) {
return c ? Either.Right() : Either.Left()
}

But to actually execute "true" and "false" branches we need to use the static function Either.either.

1
2
Either.either(g, f, truthyEither(condition))
// it is true

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
2
3
const whatToDo = Either.either(g, f)
const cond = truthyEither(condition)
whatToDo(cond)

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
2
3
npm i -D babel-cli babel-preset-es2015
echo '{"presets":["es2015"]}' > .babelrc
$(npm bin)/babel-node frp.js

First, we have same setup as before

1
2
3
import xs from 'xstream'
const f = () => console.log('it is true')
const g = () => console.log('nope')

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
2
const truthy$ = s$.filter(c => c)
const falsy$ = 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
2
3
4
5
6
truthy$.addListener({
next: f
})
falsy$.addListener({
next: g
})
1
2
3
4
$(npm bin)/babel-node frp.js
it is true
it is true
nope

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.

Related