Created
July 19, 2016 08:37
-
-
Save amonks/42a55a1785aff2703e4c62ae4fb1ce83 to your computer and use it in GitHub Desktop.
How to work with Monads IRL // Chaining in JavaScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* How to work with Monads IRL // Chaining in JavaScript | |
* by Andrew Monks // https://monks.com/monads | |
* | |
* try it in your browser: | |
* https://tonicdev.com/amonks/578beb1fdb37ac12001d3826 | |
Have you ever chained a bunch of functions together? | |
Math.log(Math.floor(Math.sqrt(Math.random()))) | |
Maybe in jQuery? | |
$('div').css('color', 'red') | |
.slideUp(2000) | |
.slideDown(2000) | |
or with promises or node streams? | |
It's a powerful, expressive, & concise way to program. | |
It's easy to do when you have a set of functions | |
thataccept an object and return an object of | |
the same type. | |
These four functions all work with jQuery DOM elements: | |
- $('div') | |
- .css('color', 'red') | |
- .slideUp(2000) | |
- .slideDown(2000) | |
These four functions all work with Numbers: | |
- Math.random | |
- Math.sqrt | |
- Math.floor | |
- Math.log | |
Check out this chain: | |
const isEven = x => x % 2 == false | |
[5].map(add(1)) | |
.map(multiply(4)) | |
.filter(isEven) | |
.map(power(2)) | |
[0] | |
Here's a few properties that arrays have that make | |
them good for chaining: | |
- there's a way to put something into an array ([]) | |
- there's a way to get something out of an array | |
([0]) | |
- in fact, `i === [i][0]` | |
- there are functions that take arrays and return | |
other arrays, like `filter(fn)` | |
- there's `.map(fn)`, which does three things: | |
- unwrap the array | |
- apply a apply regular (not array-specific) | |
function to the value inside | |
- wrap the result back up into an array | |
When a data structure has those properties ^^, it is | |
a monad. | |
jQuery DOM elements are not a monad because there | |
isn't really a way to put something in and take it | |
back out. | |
The Array Monad has some special features that | |
distinguish it from other monads, namely: | |
- it's good for holding a bunch of values in a | |
particular order | |
- it's good for finding a value if you know | |
its index | |
Monads ("chainable containers") are such a generic | |
idea that people have conceived of monads suited to | |
all sorts of particular tasks, such as: | |
- the Maybe Monad and the Either Monad, which make | |
it easy to chain functions together that might fail | |
- the Task/Future monad, which makes it easy to | |
chain functions together that perform long-running | |
or asynchronous tasks - the IO monad, which makes it | |
easy to control exactly when and how a chain of | |
functions interacts with the outside world | |
Functions within a program might all return | |
different monads. This program has a long-running | |
function that returns a Task monad, a | |
randomly-failing function that returns a Maybe | |
monad, and a logging function that returns an IO | |
monad. | |
However, functions in a chain must all accept and | |
return the same type of monad. The chain in this | |
program is based around the Task monad. We'll use | |
functions to convert the Maybe and IO monads into | |
Task monads. | |
Let's get started. | |
*/ | |
// /////////////////////////////////////////////// | |
/* Imports | |
* | |
* None of these monads is especially complex, | |
* a better article than this one could have you | |
* implementing all of them without breaking a | |
* sweat. | |
* | |
* For interoperability, most monad implementations | |
* in javascript conform to the fantasy land spec. | |
* | |
* see: https://github.com/fantasyland/fantasy-land | |
*/ | |
const Task = require('data.task') | |
const R = require('ramda') | |
const IO = require('fantasy-io') | |
const Maybe = require('data.maybe') | |
const Just = Maybe.Just | |
const Nothing = Maybe.Nothing | |
// /////////////////////////////////////////////// | |
/* Util */ | |
// depoint : a[String] -> c -> (a -> b) => String -> a -> b -> c | |
const depoint = R.curry((fnName, args, obj) => obj[fnName](...args)) | |
// /////////////////////////////////////////////// | |
/* Functions | |
* Our chain is based on the Task Monad, but that | |
* doesn't mean all our functions need to accept or | |
* return Tasks. | |
* | |
* Each of our chained functions will do kinda the | |
* same thing: build a string around its input. | |
* | |
* But, they'll each return a different type of value. | |
*/ | |
/* Non-monadic | |
* Here's a basic wrapper. Nothing monad-specific. | |
* | |
* If you aren't hip to es6, the arrow syntax might | |
* be confusing. | |
* | |
* Here's wrap without the arrows: | |
* | |
* const wrap = function (str) { | |
* return function (val) { | |
* return `${str} <NM> [${val}]` | |
* } | |
* } | |
* | |
* Call it like this: | |
* | |
* wrap('hello!')(5) | |
* // => "Hello! <NM> [5]" | |
*/ | |
// wrap : String -> a -> String | |
const wrap = str => val => `${str} <NM> [${val}]` | |
/* Task | |
* | |
* This function accomplishes the same thing, | |
* except it does it asynchronously, after a | |
* 100ms delay, and it returns a Task monad. | |
* | |
* A Task monad is a reference to a value that | |
* might not be in yet, perhaps the result of | |
* a long-running process or a network request. | |
* | |
* In this example, our chain is made up of | |
* Task Monads. | |
* | |
* If you've worked with promises, it might | |
* feel a bit familiar. | |
* | |
* Tasks in a chain won't execute until the | |
* chain reaches a `.fork()`. Scroll down to our | |
* chain and you'll see a fork at the end. | |
* | |
* If you chain a bunch of tasks together | |
* and one fails, the chain will break and skip | |
* to the next chained `.fork()`. | |
* | |
* At the fork, you can handle the errors and | |
* resume processing (or not). | |
* | |
* The Task monad also provides ways to do | |
* things we won't here, like: | |
* | |
* - pause a chain until several concurrent | |
tasks are complete | |
* - sort tasks by how long they took | |
*/ | |
// wrapSlowly : String -> String -> Task(_, String) | |
const wrapSlowly = msg => | |
str => | |
new Task((reject, resolve) => | |
setTimeout(() => resolve(`${msg} [<TASK> ${str}]`), 100) | |
) | |
/* IO | |
* | |
* This function wraps the text too, but it | |
* also logs its input and returns an IO monad. | |
* | |
* IO monads are like the delayed execution | |
* (`.fork()`) part of Task Monads, minus the | |
* error handling and the asynchronicity. | |
* | |
* You can use them to controlwhen and where | |
* certain procedures (usually IO) are performed. | |
* | |
* You can chain IO monads together, and they | |
* won't be executed until the chain reaches a | |
* `.unsafePerform()`. | |
* | |
* In some languages (like Haskell), functions | |
* _can't_ have any side effects unless they're | |
* wrapped in an IO Monad. | |
* | |
* It sounds niche, but there are cases (like | |
* working with library code) when it's very | |
* comforting to know that nothing will happen | |
* until you explicitly tell it to. | |
*/ | |
// wrapAndLog :: String -> a -> IO( String ) | |
const wrapAndLog = msg => val => IO(() => { | |
console.log(`log) | |
return `${msg} <IO> [${val}]` | |
}) | |
/* Maybe | |
* | |
* This one's fun. Half the time it just returns | |
* the wrapped string, like the other functions, | |
* but there's a 50% chance it will return nothing. | |
* | |
* The Maybe monad is among the most useful. It | |
* consists of two types: Just (think 'only', like | |
* Just(5)) and Nothing. | |
* | |
* A function in a Maybe monad chain will only | |
* execute if its passed a Just. If it's passed a | |
* Nothing, it'll silently pass the Nothing right | |
* along. | |
* | |
* See also: | |
* The Either monad is similar, but more generic. | |
* A function in an Either chain can return one of | |
* two definable types. Usually the type it was passed | |
* _or_ an Exception, which is more detailed | |
* than Nothing. | |
*/ | |
// wrapMaybe : String -> a -> Maybe(String) | |
const wrapMaybe = msg => val => | |
Math.random() > 0.5 ? Just(`${msg} <JUST> [${val}]`) | |
: Nothing() | |
// /////////////////////////////////////////////// | |
/* Convert Other Monads to Tasks! | |
* We're building our chain around the Task Monad, | |
* so we'll need to be able to turn our other monads | |
* into Task Monads. | |
* | |
* Putting some value (like another monad) into a | |
* monad container is called "lifting" it. | |
*/ | |
// IOToTask : (IO) -> a -> Task[_ a] | |
const IOToTask = io => | |
v => new Task((reject, resolve) => | |
R.compose(resolve, depoint('unsafePerform', [undefined]), io)(v) | |
) | |
// MaybeToTask : Maybe -> Task | |
const MaybeToTask = m => v => { | |
const maybied = m(v) | |
return maybied.isNothing ? Task.rejected('error') | |
: Task.of(maybied.value) | |
} | |
const wrapAndLog_task = R.compose(IOToTask, wrapAndLog) | |
const wrapMaybe_task = R.compose(MaybeToTask, wrapMaybe) | |
// /////////////////////////////////////////////// | |
/* Chain it! | |
* We're good to go. This is a Task-based chain, | |
* so we'll start by lifting (converting) a string | |
* literal into a Task | |
*/ | |
Task.of('0 <INIT>') | |
.map(wrap('2')) | |
.chain(wrapSlowly('3')) | |
.chain(wrapAndLog_task('4')) | |
.chain(wrapMaybe_task('5')) | |
.chain(wrapAndLog_task('6')) | |
.fork( | |
val => console.log(`7a <FAILURE> [${val}]`), | |
val => console.log(`7b <SUCCESS> [${val}]`) | |
) | |
// /////////////////////////////////////////////// | |
/* Bonus: Pointfree style | |
* With a couple extra utilities, we can | |
* compose our chain into a function | |
* | |
* This style is called "pointfree" because | |
* you don't have to refer to the "point" | |
* of the function (aka the initial value) | |
*/ | |
const map = fn => depoint('map', [fn]) | |
const chain = fn => depoint('chain', [fn]) | |
// compose works inside out, to match how | |
// functions would be nested: | |
// | |
// f(g(x)) === R.compose(f, g)(x) | |
const app = R.compose( | |
depoint('fork', [ | |
val => console.log(`7a <FAILURE> [${val}]`), | |
val => console.log(`7b <SUCCESS> [${val}]`) | |
]), | |
chain(wrapAndLog_task('6')), | |
chain(wrapMaybe_task('5')), | |
chain(wrapAndLog_task('4')), | |
chain(wrapSlowly('3')), | |
map(wrap('2')), | |
Task.of | |
) | |
app('0 <INIT>') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment