Maybes allow us to gracefully work with data that might be null and to have defaults. A maybe is a variable that either has some value or it doesn't. And it doesn't matter to the caller.
On its own, it might seem like this is not that big a deal. Everybody knows that null-checks are easily accomplished with an if-else
statement:
if (getUsername() == null ) { username = 'Anonymous') { else { username = getUsername(); }
But with functional programming, we're breaking away from the procedural, line-by-line way of doing things and instead working with pipelines of functions and data. If we had to break the chain in the middle just to check if the value existed or not, we would have to create temporary variables and write more code. Maybes are just tools to help us keep the logic flowing through the pipeline.
To implement maybes, we'll first need to create some constructors.
// the Maybe monad constructor, empty for now var Maybe = function(){}; // the None instance, a wrapper for an object with no value var None = function(){}; None.prototype = Object.create(Maybe.prototype); None.prototype.toString = function(){return 'None';}; // now we can write the `none` function // saves us from having to write `new None()` all the time var none = function(){return new None()}; // and the Just instance, a wrapper for an object with a value var Just = function(x){return this.x = x;}; Just.prototype = Object.create(Maybe.prototype); Just.prototype.toString = function(){return "Just "+this.x;}; var just = function(x) {return new Just(x)};
Finally, we can write the maybe
function. It returns a new function that either returns nothing or a maybe. It is a functor.
var maybe = function(m){ if (m instanceof None) { return m; } else if (m instanceof Just) { return just(m.x); } else { throw new TypeError("Error: Just or None expected, " + m.toString() + " given."); } }
And we can also create a functor generator just like we did with arrays.
var maybeOf = function(f){ return function(m) { if (m instanceof None) { return m; } else if (m instanceof Just) { return just(f(m.x)); } else { throw new TypeError("Error: Just or None expected, " + m.toString() + " given."); } } }
So Maybe
is a monad, maybe
is a functor, and maybeOf
returns a functor that is already assigned to a morphism.
We'll need one more thing before we can move forward. We'll need to add a method to the Maybe
monad object that helps us use it more intuitively.
Maybe.prototype.orElse = function(y) { if (this instanceof Just) { return this.x; } else { return y; } }
In its raw form, maybes can be used directly.
maybe(just(123)).x; // Returns 123 maybeOf(plusplus)(just(123)).x; // Returns 124 maybe(plusplus)(none()).orElse('none'); // returns 'none'
Anything that returns a method that is then executed is complicated enough to be begging for trouble. So we can make it a little cleaner by calling on our curry()
function.
maybePlusPlus = maybeOf.curry()(plusplus); maybePlusPlus(just(123)).x; // returns 123 maybePlusPlus(none()).orElse('none'); // returns none
But the real power of maybes will become clear when the dirty business of directly calling the none()
and just()
functions is abstracted. We'll do this with an example object User
, that uses maybes for the username.
var User = function(){ this.username = none(); // initially set to `none` }; User.prototype.setUsername = function(name) { this.username = just(str(name)); // it's now a `just }; User.prototype.getUsernameMaybe = function() { var usernameMaybe = maybeOf.curry()(str); return usernameMaybe(this.username).orElse('anonymous'); }; var user = new User(); user.getUsernameMaybe(); // Returns 'anonymous' user.setUsername('Laura'); user.getUsernameMaybe(); // Returns 'Laura'
And now we have a powerful and safe way to define defaults. Keep this User
object in mind because we'll be using it later on in this chapter.