Maybe monad in JavaScript to save us from the hell of the null guard clauses
If one were asked to choose a single functional data structure from all the available ones, I bet they would choose the redoubtable Maybe, or Option under its other incarnation in Scala. This Maybe monad is in essence a simple box of computation that checks whether the data that it stores inside has or has no defined value and, based on this, either continues to apply further transformations to the value within or simply ignores them and safely routes the absence of value toward the finale of the computation chain.
Example 1
Maybe.of(undefined)
.map(value => value * 2)
.map(value => value * value)
.getOrElse(0) === 0
Maybe.of(2)
.map(value => value * 2)
.map(value => value * value)
.getOrElse(0) === 16
will both print true. The last method getOrElse(null)
, which simply unboxes the value or returns null if there is none. So we implement the null guard clause only once inside the map
method without any need to have these null value checks scattered throughout all the callbacks passed to map
in the above chain. Here is a short implementation.
Example 2
class Maybe {
constructor(value) {
this.__value = value;
}
static of(valueToBox){
return new Maybe(valueToBox);
}
getOrElse(elseVal) {
return this.isNothing() ? elseVal : this.__value;
}
isNothing() {
return this.__value === null || this.__value === undefined;
}
map(fn) {
return this.isNothing()?
Maybe.of(null):
Maybe.of(fn(this.__value));
}
}
We have now contained all our null checking logic in one place, reducing the number of possible errors in the future.
Tip.
You’ll soon see a lot ofgetOrElse(null)
and getOrElse([])
calls in your code. They can be optimized into:...
getOrNull() {
return this.getOrElse(null);
}
...
and
...
getOrEmptyArray() {
return this.getOrElse([]);
}
...
There is one caveat, though. What if somewhere in the map chain we have a callback that returns another Maybe?
Example 3
const mbMapper = x => Maybe.of(x).map(_x => _x * 2);
//-> returns a Maybe
Maybe.of(2).map(mbMapper).getOrElse(0) === ?
Our result is Maybe(4)
We’d apparently need a second call to getOrElse()
method to retrieve the value. Such constructions would soon pollute our code. Moreover it would effectively prevent our Maybe
type from being a monad in the mathematical sense of the term. As a monad must flatten the monad hierarchy flowing through its chain. To tackle this problem we’ll introduce a new method: flatMap
.
Example 4
...
flatMap(fn){
if(this.isNothing()) return Maybe.Nothing();
const m = fn(this.__value);
return m.isNothing() ?
Maybe.Nothing() :
Maybe.of(m.__value);
}
...
Now we can do
const mbMapper = x => Maybe.of(x).map(_x => _x * 2);
Maybe.of(2).flatMap(mbMapper).getOrElse(0) === 4 // prints true
So the whole implementation of our Maybe monad looks now like:
Example 5
class Maybe {
constructor(value) {
this.__value = value;
}
static of(valueToBox){
return new Maybe(valueToBox);
}
flatMap(fn){
if(this.isNothing()) return Maybe.Nothing();
const m = fn(this.__value);
return m.isNothing() ?
Maybe.Nothing() :
Maybe.of(m.__value);
}
getOrElse(elseVal) {
return this.isNothing() ? elseVal : this.__value;
}
getOrEmptyArray() {
return this.getOrElse([]);
}
getOrNull() {
return this.getOrElse(null);
}
isNothing() {
return this.__value === null || this.__value === undefined;
}
map(fn) {
return this.isNothing()?
Maybe.of(null):
Maybe.of(fn(this.__value));
}
}
All that is left now is to ensure that we are indeed dealing with a monad here. To be considered as such our Maybe structure needs to satisfy the following laws (using chai testing library syntax):
- Left identity:
it("left identity law should be satisfied", () => { const value = 2; const mbValue = Maybe.of(value); const mapper = x => Maybe.of(x * 2); expect(mbValue.flatMap(mapper)).to.deep.equal(mapper(value)); });
- Right identity:
it("right identity law should be satisfied", () => { const mbValue = Maybe.of(2); const mapper = x => Maybe.of(x); expect(mbValue.flatMap(mapper)).to.deep.equal(mbValue); });
- Associativity:
The above tests prove that our Maybe is none other than a monad.it("associativity law should be satisfied", () => { const f = val => Maybe.of(val + 1); const g = val => Maybe.of(val * 2); const m = Maybe.of(1); const lhs = m.flatMap(f).flatMap(g); const rhs = m.flatMap(x => f(x).flatMap(g)); expect(lhs).to.deep.equal(rhs); });
If you're passionate about Front End development, check out our JavaScript job-board here!
Originally published on medium.com
Related Jobs
Related Articles
Related Issues
- Started
- 0
- 16
- Intermediate
- Submitted
- 1
- 0
- Intermediate
Get hired!
Sign up now and apply for roles at companies that interest you.
Engineers who find a new job through Functional Works average a 15% increase in salary.
Start with GitHubStart with TwitterStart with Stack OverflowStart with Email