Would you like to clone this notebook?

When you clone a notebook you are able to make changes without affecting the original notebook.

Cancel

Whose afraid of the that big bad wolf?

node v10.24.1
version: 1.0.0
endpointsharetweet
/* # Whose afraid of the that big bad wolf? Welcome to fantasy land; this post is about that big bad wolf; the monad. More specifically, about a monad called Maybe. There are other types of monads; for instance, the Either and IO monads. Which have been developed to deal with different circumstances within computer programs. Why big bad wolf? Well, monads can be a bit scary; it's not, how you say, idiomatic in JavaScript. So this word monad, if you search the internet, is connected with phrases such as: "Once you finally understand monads, you lose the ability to explain monads to others." and... "A monad is just a monoid in the category of endofunctors, what's the problem?" It turns out that these comments about monads are, of course, jokes to make fun about the difficulty people have with explaining monads to others. You kind of get the feeling your in for a hard time with the monad, even before you start! So, if you are venturing down that "rabbit hole" into the fantasy land of abstract data types(ADT's) then I hope that I can give you some idea about the concept of a monad without needing to dive into category theory or telling you that you must learn Haskell before you can understand a monad. In this post I hope that, for the newcomer(and let's face it, we are all newcomers at something), I can show you a "bridge" between your present JavaScript knowledge and the use of the Maybe monad in your code. The only prerequisite being that you are familiar with JS Array methods, mainly .map() and the JS Promise object; additionally, I assume you have used functional composition and currying. ## So what is a monad A monad is an object that wraps a value in a context. Let's explain that in terms that a JS programmer should understand. Here is an analogy; an Array, as we know, is a collection of elements, like this [0, 1, 2, 3, 4, 5] Here is an Array of only one element. */ const M = [2] // M is a wrapped value; it has a context. The context is the Array. Array has a large number of methods that it provides. // I need some functions to work with const log = console.log const sq = n => n * n // add - a curried version of the add function. const add = a => b => a + b // a new function made by partially applying the add function. const add10 = add(10) log(typeof add10) // => function // we can "map over" this wrapped value ie. apply a function to the value to transform it to another value. const result = M .map(add10) .map(sq) log(result) // => [ 144 ] // [144], this result may not be what you want, it is still wrapped. So in JS, the way to get at the value is .pop() log(result.pop()) // => 144 /* So what has the ability to .map given us? It has given us a way to transform a value by applying functions to the value and then, repeatedly, doing the same thing again. This is not function composition, but value transformation. Incidentally, an object with a .map method is referred to as a functor. So an Array is a functor. Also, a monad is a functor; and more. At this stage if you are still not quite sure what I am going on about then [this](http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html) might help by giving a pictorial view of what is happening. So what exactly did happened in the code above? These are the steps: - a wrapped value([2]) was mapped over by taking the value 2 out of the Array context and applying the function(add10) to the value - the result of the function application(12) was then wrapped back into the Array context - the next .map was treated in the same way unwrap 12, apply function sq, value is now 144, wrap it back into the Array. */ // Let's make a function, using a similar approach to the above, but operating on a String value. const exclaim = s => s + '!' const exclaimWorld = val => // .of lifts the value into the Array Array.of(val) .map(s => s.concat(' World')) .map(exclaim) .pop() // ...and call it. log(exclaimWorld('Dave\'s')) // => Dave's World! /* Well, this is all very fine and dandy, but what if that value is'nt there. Then what? */ const o = { val: "Hello", nil: null, udef: undefined } log(exclaimWorld(o.val)) // => Hello World! // - OK, good it worked because the value passed in is a value. // Now watch carefully... // log(exclaimWorld(o.udef)) // => TypeError: Cannot read property 'concat' of undefined. // BOOM! our program is dead, now we need that Maybe! We cannot do undefined.concat() this is the problem. /* Yes, we need that Maybe, but first, let's look at one more analogy; this time using an object JS programmers should know about, the Promise object. I will call this pMaybe to distinguish it as being derived using a Promise. Please note that this is just for training purposes to show the similarity of a Maybe to other common objects in JS. We have seen that Array gives us one property of a Maybe; the ability to .map over a value applying whatever function to the value as required ie. Maybe is a functor. But fails to provide protection against undefined/null situations. The Promise, however, allows us to follow one of two paths that is normally called resolve and reject, but here I rename them Just and Nothing. Yes, I am slightly abusing the normal use case for the Promise, but it will hopefully aid our understanding. */ const pMaybe = val => new Promise( (Just, Nothing) => { if (val === undefined || val === null) Nothing(val) Just(val) } ) const pExclaimWorld = s => pMaybe(s) .then(s => s.concat(' World')) .then(exclaim) // .then(s => undefined) .then(s => console.log(s)) .then(s => s.concat('Can we do this?')) .catch((e) => console.log(`You passed in ${e} this is a likely cause of error!` )) pExclaimWorld(o.val) // => Hello World! pExclaimWorld('Brian\'s') // => Brian's World! pExclaimWorld(o.udef) // => You passed in undefined this is a likely cause of error! // to make our code more composable we need some curry! Here's a curry function const curry = f => (...args) => args.length >= f.length ? f(...args) : curry(f.bind(undefined, ...args)) // a prop function const prop = curry((k, o) => o[k]) // Another example const add10ToAge = personO => pMaybe(personO) .then(prop('age')) .then(add(10)) .then(result => console.log(result)) .catch(e => console.log(`Nothing , check your input`)) add10ToAge({ name: 'Dinah', age: 14 }) // => 24 // Note: The results from the above will appear later in the output because of the effect of the Promise. /* Now you see this time our code does not err. The potential threat of undefined on our code is mitigated by the .catch() and a undefined or null is handled with a friendly response. This has a severe limitation though and that is the undefined/null checking only happens on the initial lifting of the value into pMaybe. If a null or undefined is generated as a result of any of the following .then calls it will not produce a Nothing ...and that's not good. To see this in action change the 'age' prop to 'ag', which is not a property on the passed in object. It now will return Just(NaN). What we really wanted is Nothing, indicating the code failed due to undefined. By now I am hoping that you are getting the idea about the Maybe monad. I have tried to relate it to something that a JS coder should already know. But, fear not if you are still not comfortable with it, there are still more examples to come. Now we know what problem the Maybe is designed to solve; the absence of a value(ie. undefined or null which is a common cause of bugs in our code). We have also discovered that a Maybe has the properties of an Array(the ability to map) and the branching ability of a Promise, but they both fall short. So, let's go for it. Let's do an actual Maybe in JavaScript and here it is... */ const Maybe = (function () { const Just = function (x) { this.x = x; }; Just.prototype.map = function (fn) { return Maybe.of(fn(this.x)) }; Just.prototype.chain = function (fn) { return fn(this.x) }; Just.prototype.toString = function () { return `Just(${this.x})` }; const Nothing = function () {}; Nothing.map = () => Nothing; Nothing.chain = () => Nothing; Nothing.toString = () => 'Nothing'; return { of: (x) => x === null || x === undefined ? Nothing : new Just(x), lift: (fn) => (...args) => Maybe.of(fn(...args)), Just, Nothing }; })(); /* So, in less than twenty lines of code a Maybe is laid bare here in vanilla JS. Study it carefully; it is a thing of beauty! This is still not a fantasy-land compliant Maybe, however, it is a huge improvement and quite useable in our code. Later, we will swap to using the folktale Maybe, so stick around for that. Right, how does it work? You can see that it has a Just function for handling the happy path(the presence of a value) and a Nothing function to handle the case when a value is absent. Mapping when the Maybe is routed down the happy path causes a provided function to be applied to the value passed into the Maybe; whereas no such function application occurs when mapping on the Nothing path. This is where the protection from errors due to undefined/null hapens. Finally, the Maybe returns an object so that the user can interact with it. Also, please note, that each time we .map we rewrap the result by doing Maybe.of which ensures undefined/null checking continues to happen. .of() is usd to lift a value into the Maybe. .lift() is used to lift a fuction and its arguments into the Maybe. Better than explaining it line by line, let's see how we can use it to solve the problem of the exclaim function above. Let's make a safeExclaim function. */ const safeExclaimWorld = val => Maybe.of(val) .map(s => s.concat(' World')) .map(exclaim) .chain(x => x) .toString() log(safeExclaimWorld('Bob\'s')) // => Bob World! log(safeExclaimWorld(o.udef)) // => Nothing // => this time we don't get a TypeError. That's good! The Maybe justifies its existence. /* Notice how similar the exclaim and the safeExclaim functions are. In particular you should note the following points: - As you know, a value is lifted into the Maybe using .of(); you can liken this to Array.of() - Value transformation is provided using .map(); in a similar fashion to Array .map() - Now we come to the line .chain(x => x); this gets our value out of the Maybe, similar to .pop() with the Array version. Incidentally, the anonymous function x => x is commonly referred to as the identity function(const identity = x => x). .chain flattens the Maybe and the value pops out. - .chain() needs to be passed a function to work. I the above situation we don't want to transform the value, so hence identity is used. */ /* Example to show another way Maybe can be used The Array method .find() will find the first occurence of an element in an array or return undefined. Let's imagine we have an array of bears and want to find a specific bear in a safe manner. */ const bears = ['Black', 'Grizzly', 'Kodiac', 'Polar', 'Spectacled', 'Sloth'] const find = curry((f, xs) => Array.prototype.find.call(xs, f)) // .lift() - lifts a function and its arguments into a Maybe // safeFind : Array -> Any -> Any|Object const safeFind = curry((xs, val) => Maybe.lift(find)(el => el === val, xs) .chain(x => x)) console.log(safeFind(bears, 'Kodiac')) // => Kodiac console.log(safeFind(bears)('Polar')) // => Polar console.log(safeFind(bears)('Big') ) // => a Nothing object // Another example demonstrates that safeFind will work for other types of values. const integers = [34, 72, 56, 82, 94, 27, 11, 45, 42, 77, 90, 55] const finder = curry((coll, x) => safeFind(coll, x)) const intFinder = finder(integers) console.log(intFinder(42)) // => 42 console.log(intFinder(94)) // => 94 console.log(intFinder(423)) // => a nothing object /* At this point I encourage you to copy this code, play around with it and make up your own examples. Doing this will help your understanding far more than just reading this article. */
Loading…

no comments

    sign in to comment