Building A Maybe In JavaScript

alsiola
1,107 views

Open Source Your Knowledge, Become a Contributor

Technology knowledge has to be shared and made accessible for free. Join the movement.

Create Content

Cannot read property "x" of undefined. I'm guessing if you're reading this you've seen that message before, and probably at some point wanted to throw something through your monitor. We have the venerable Sir Tony Hoare to thank for this - he invented the null reference in 1965 while creating ALGOL-C. In fairness, he later went on to say

"I call it my billion-dollar mistake."

Fortunately there are some functional programming techniques we can use to ease the pain in a clean, concise and reliable way. Let's imagine we want to extract the value of the property "c" from the following object, and append the string " is great". We might take this simple approach:

1
2
3
4
5
6
7
8
9
10
11
12
const a = {
b: {
c: "fp"
}
};
const appendString = (obj) =>
obj.b.c + " is great";
const result = appendString(a);
console.log(result);
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

This works fine, but sadly we must live in the real world. As a result the data we get back will sometimes take these forms:

const a = {
    b: {}
};

// or

const a = {};

If we apply our appendString function to these values of a, things are going to explode...

Cannot read property "c" of undefined.

The imperative approach to this problem might be to add some null checking to our function:

1
2
3
4
5
6
7
8
9
10
const appendString = (obj) => {
if (!obj || !obj.b || !obj.b.c) return null;
return obj.b.c + " is great";
}
const badInput = { b : {} };
const goodInput = { b : { c : "FP" }};
console.log("Bad Input", appendString(badInput));
console.log("Good Input", appendString(goodInput));
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

This works, but its ugly and error-prone. We also have to define specific (correct) null checks for every type of object we want to parse, which just isn't much fun. Here's where the Maybe can come to the rescue.

Our basic maybe

Essentially the maybe object we're going to construct will encapsulate the concept that its value may be null, and take care of the complexity that ensues. Following the Elm lead, we'll call Maybe's two conceptual states Maybe.just and Maybe.nothing. For starters, let's simply define an isNothing method that returns a boolean telling us if the Maybe contains nothing, and an extract method to return the value inside for debugging.

const isNullOrUndef = (value) => value === null || typeof value === "undefined";

const maybe = (value) => ({
    isNothing: () => isNullOrUndef(value),
    extract: () => value
});

And a simple factory function to create our maybes - as we'll want to add some more methods later we'll define this on an object:

const Maybe = {
    just: maybe,
    nothing: () => maybe(null)
};

So now we can do this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const isNullOrUndef = (value) => value === null || typeof value === "undefined";
const maybe = (value) => ({
isNothing: () => isNullOrUndef(value),
extract: () => value
});
const Maybe = {
just: maybe,
nothing: () => maybe(null)
};
const maybeNumberOne = Maybe.just("a value");
const maybeNumberTwo = Maybe.nothing();
console.log("Maybe.just is nothing?", maybeNumberOne.isNothing());
console.log("Maybe.nothing is nothing?", maybeNumberTwo.isNothing());
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

All well and good, but not very useful so far. Programming is all about transforming data, so we need to have a way of transforming our maybes - a map function. This map function will take a function argument representing the transformation we wish to make, and return a new maybe containing the result of the transformation. Importantly, if the maybe wraps nothing, then the function won't be applied, we'll just return a new maybe.nothing:

const maybe = (value) => ({
    isNothing: () => isNullOrUndef(value),
    extract: () => value,
    map: (transformer) => !isNullOrUndef(value) ? Maybe.just(transformer(value)) : Maybe.nothing()
});

Now we can use this to work with our maybes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const isNullOrUndef = (value) => value === null || typeof value === "undefined";
const maybe = (value) => ({
isNothing: () => isNullOrUndef(value),
extract: () => value,
map: (transformer) => !isNullOrUndef(value) ? Maybe.just(transformer(value)) : Maybe.nothing()
});
const Maybe = {
just: maybe,
nothing: () => maybe(null)
};
const maybeOne = Maybe.just(5);
const mappedJust = maybeOne.map(x => x + 1);
console.log(mappedJust.extract());
const maybeTwo = Maybe.nothing();
const mappedNothing = maybeTwo.map(x => x + 1);
console.log(mappedNothing.extract());
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

A key point is that maybe#map returns a new maybe, so we can chain these operations together. Going back to our original problem we could now do:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const isNullOrUndef = (value) => value === null || typeof value === "undefined";
const maybe = (value) => ({
isNothing: () => isNullOrUndef(value),
extract: () => value,
map: (transformer) => !isNullOrUndef(value) ? Maybe.just(transformer(value)) : Maybe.nothing()
});
const Maybe = {
just: maybe,
nothing: () => maybe(null)
};
const a = {
b: {
c: "fp"
}
};
const maybeA = Maybe.just(a)
.map(a => a.b)
.map(b => b.c)
.map(c => c + " is great!");
console.log(maybeA.extract());
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

The great thing here is that if any of the steps in the chain return null, we still get a result of Maybe.nothing at the end, rather than a runtime error.

Point-free chaining

We can improve the process again by using higher-order functions for extracting named properties from an object, and for appending a string:

const prop = (propName) => (obj) => obj[propName];
const append = (appendee) => (appendix) => appendee + appendix;

So now we can use this function in our map chain:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const isNullOrUndef = (value) => value === null || typeof value === "undefined";
const maybe = (value) => ({
isNothing: () => isNullOrUndef(value),
extract: () => value,
map: (transformer) => !isNullOrUndef(value) ? Maybe.just(transformer(value)) : Maybe.nothing()
});
const Maybe = {
just: maybe,
nothing: () => maybe(null)
};
const prop = (propName) => (obj) => obj[propName];
const append = (appendee) => (appendix) => appendix + appendee;
const a = {
b: {
c: "fp"
}
};
const maybeA = Maybe.just(a)
.map(prop("b"))
.map(prop("c"))
.map(append(" is great!"));
console.log(maybeA.extract());
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

We're getting somewhere now - we have null-proofed our extraction process and refactored it to a point-free style. All we need to do now is make our logic re-usable - what we want is to be able to pass our maybe to a function and have all the steps applied so we can re-use the extractor on many different maybes:

    const extractor = // what we're about to make
    extractor(Maybe.just(a)); // Maybe.just("fp is great")

What we need is a function that takes our steps, and sequentially calls the #map method of our Maybe with each step. We'll call the function Maybe#chain, and we can implement it with a reducer:

const Maybe = {
    just: maybe,
    nothing: () => maybe(null),
    chain: (...fns) => (input) => fns.reduce((output, curr) => output.map(curr), input)
};

We can now build a re-usable function that can be applied to maybes, and use it on a variety of inputs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const isNullOrUndef = (value) => value === null || typeof value === "undefined";
const maybe = (value) => ({
isNothing: () => isNullOrUndef(value),
extract: () => value,
map: (transformer) => !isNullOrUndef(value) ? Maybe.just(transformer(value)) : Maybe.nothing()
});
const Maybe = {
just: maybe,
nothing: () => maybe(null),
chain: (...fns) => (input) => fns.reduce((output, curr) => output.map(curr), input)
};
const prop = (propName) => (obj) => obj[propName];
const append = (appendee) => (appendix) => appendix + appendee;
const appendToC = Maybe.chain(
prop("b"),
prop("c"),
append(" is great!")
);
const goodInput = Maybe.just({
b: {
c: "fp"
}
});
const badInput = Maybe.just({});
console.log(appendToC(goodInput).extract())
console.log(appendToC(badInput).extract());
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Although I wasn't accustomed to the concept of Maybe values before learning Elm, they are something I didn't want to leave behind when working in JavaScript. If we are going to use them in a truly universal way then we will need to add some more functionality, so I'll be writing a follow up post soon looking at working with arrays of Maybe values.

Open Source Your Knowledge: become a Contributor and help others learn. Create New Content