"Once you understand monads, you immediately become incapable of explaining them to anyone else" Lady Monadgreen’s curse ~ Gilad Bracha (used famously by Douglas Crockford)
A monad is a way of composing functions that require context in addition to the return value, such as computation, branching, or I/O. Monads type lift, flatten and map so that the types line up for lifting functions
a => M(b)
, making them composable. It's a mapping from some typea
to some typeb
along with some computational context, hidden in the implementation details of lift, flatten, and map.
a => b
Functor(a) => Functor(b)
Monad(Monad(a)) => Monad(b)
What does "flatten", "map" and "context" mean?
a => b
to the value inside the context, and return a new value b
wrapped inside the same kind of context. Ie Array(a) => Array(b)
, Observable(a) => Observable(b)
.a => F(a)
.The associated example from the blog that sets Array
as the context and x
as the value we're mapping over:
const x = 20; // Some data of type `a` const f = n => n * 2; // A function from `a` to `b` const arr = Array.of(x); // The type lift. // JS has type lift sugar for arrays: [x] // .map() applies the function f to the value x // in the context of the array. const result = arr.map(f); // [40]
The example did not include an array of arrays, but that can still be flattened:
[1], [2, 3], [4](/javascript/1], [2, 3], [4).flat(); // [1, 2, 3, 4] or [].concat.apply([], [1], [2, 3], [4](/javascript/1], [2, 3], [4)); // [1, 2, 3, 4]
a => b
which lets you compose functions of type a => b
Functor(a) => Functor(b)
, which lets you compose functions F(a) => F(b)
Monad(Monad(a)) => Monad(b)
, which lets you compose lifting functions a => F(b)
// composing functiond g: a => b f: b => c h = f(g(a)): a => c // composing functors g: F(a) => F(b) f: F(b) => F(c) h = f(g(Fa)): F(a) => F(c) // composing functions without Monads - uh oh g: a => M(b) f: b => M(c) h = composeM(f, g): a => M(c) // f was expecting b, but got M(b) // so we us the flatten map process, // often called .bind() or .chain() g: a => M(b) flattens to => b f: b maps to => M(c) h composeM(f, g): a flatten(M(b)) => b => map(b => M(c)) => M(c)
Let's go a real world example of compose
vs composeM
:
// Using compose const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x); const trace = label => value => { console.log(`${label}: ${value}`); return value; }; const label = 'API call composition'; // a => Promise(b) const getUserById = id => id === 3 ? Promise.resolve({ name: 'Kurt', role: 'Author' }) : undefined; // b => Promise(c) const hasPermission = ({ role }) => Promise.resolve(role === 'Author'); // Try to compose them. Warning: this will fail. const authUser = compose( hasPermission, getUserById, ); // Oops! Always false! authUser(3).then(trace(label)); // Using composeM const composeM = chainMethod => (...ms) => ms.reduce((f, g) => x => g(x)[chainMethod](f)); const composePromises = composeM('then'); const label = 'API call composition'; // a => Promise(b) const getUserById = id => id === 3 ? Promise.resolve({ name: 'Kurt', role: 'Author' }) : undefined; // b => Promise(c) const hasPermission = ({ role }) => Promise.resolve(role === 'Author'); // Compose the functions (this works!) const authUser = composePromises(hasPermission, getUserById); authUser(3).then(trace(label)); // true
A monad is based on a simple symmetry — A way to wrap a value into a context, and a way to unwrap the value from the context.
a => M(a)
.M(a) => a
.Since Monads are also functors, they can also map:
M(a) -> M(b)
.Combining flatten with map, you get chain
- function composition for monad-lifting functions (Kleisli composition):
M(M(a)) => M(b)
.// Identity monad const Id = value => ({ // Functor mapping // Preserve the wrapping for .map() by // passing the mapped value into the type // lift: map: f => Id.of(f(value)), // Monad chaining // Discard one level of wrapping // by omitting the .of() type lift: chain: f => f(value), // Just a convenient way to inspect // the values: toString: () => `Id(${value})`, }); // The type lift for this monad is just // a reference to the factory. Id.of = Id;