by Bruce Pascoe - 1 May, 2019
"A monad is just a monoid in the category of endofunctors. What's the problem?" ~James Iry1
The problem... is that there are several problems.
It's been said that monads bear a dreadful curse. Once you finally understand what they are, you begin to see them everywhere--but somehow become completely incapable of explaining them to anyone else. Many tutorial writers have tried to break the Great Curse--the Web is lousy with bold attempts and half successes that attest to this--and just as many have failed. Well, I'm here to address the elephant in the room2 and tell you that I intend to break the Great Curse once and for all.
There are basically two ways a monad tutorial tends to go. One is a paragraph or two of minimal descriptions of one or two common monads (Haskell's Maybe
in particular is very popular), followed by a lot of intimidating Haskell syntax trying to explain--precisely--how it all fits together. This is well-intentioned but misguided; while we as programmers do tend to think in code, and pseudocode can convey a lot of information very quickly, in this case it's for naught; the underlying programming paradigm Haskell exemplifies is entirely different from how someone coming from an imperative or object-oriented background intuitively thinks about their "world". The tutorial writer may as well be speaking gibberish for all the code samples are worth in practice.
The other way this can go also involves a bunch of Haskell syntax, but first tries to explain the idea of a monad by rigorous mathematical definition, usually by stating the laws they obey (it can be worse--some just dive right into category theory nonsense!). This, at least in my experience, is perhaps even worse than the above; while I do now know what a monad is (and I'm going to teach you!), I still don't find the laws themselves that useful for reasoning about how monads behave--much less for understanding what they represent. Besides, telling someone the identity laws right off the bat, even assuming they understood them, doesn't give them any information on why they should want these things in their programs in the first place--or why they should even care!
So I'm going to try a different approach, one that acknowledges the ways in which imperative programmers (like me!) already think about their world and avoids getting into advanced mathematics that won't help for understanding the concepts in real-world terms anyway. If you can multiply numbers together and know what a function is, you can understand monads, I promise you.
Without further ado, onto the tutorial!
In most imperative and object-oriented languages, a function can be thought of as a subroutine that takes one or more values as input (called parameters), perhaps performs some action, and produces a single value (called the return value) as output. We can call up the function as many times as we want, with whatever inputs we want, and get the correct output.
Some people will go out of their to way to stress that the mathematical definition of a function is much narrower than how programmers use the term (mathematical functions don't permit "side effects", for example, and always return the same output for the same input), but that's not relevant to us. Anyone who tells you functional purity is important for understanding monads in a programming context is lying to you (and probably has an agenda).
But I digress. Functions are very useful:
function add(x, y) {
return x + y;
}
let x = add(2, 2); // assigns 4 to variable x
let y = add(3, 3); // assigns 6 to variable y
We already know why this is useful: A computation may consist of several lines of code, and the code to perform that computation doesn't particularly care what the exact value is, so why write it more than once?
So a function lets us write code once to handle any value (or values) we want to throw at it, as many times as we need to do so. The only restriction, if any, is that the value is always a particular type--for example, a string, or a number. This is fine: The code to do a similar computation on a different type would necessarily be different, so we can afford to write another function to handle that.
Often though (way more often than we consciously realize!), we find ourselves writing exactly the same code, over and over and over again, regardless of the type of values involved. For example, we might write a for
loop to process all the elements of an array:
// old school!
for (let i = 0; i < list.length; i++) {
let value = list[i];
// do stuff with each value
}
// new style using ES6+ iterators, shiny!
// (but we still have to write the loop every time...)
for (let value of list) {
// do stuff with each value
}
Or, perhaps we're calling functions that can return null
instead of a meaningful value to indicate failure. If at any point we get a null
, we have to bail out because the world is no longer in the state we expect and it's meaningless to continue (i.e. the null
is not actually a real value we can work with, it's just being used as a sentinel):
let file = openFile("log_file.txt");
if (file === null)
return false;
let result;
if (f.write(file, "day #9001: a pig ate everyone today.") === null)
return false;
if (f.close(file) === null)
return false;
And these are just the tip of the iceberg. Suffice to say there are plenty of cases where we write the same code over and over again, no matter what kind of data we're working with. It doesn't matter if that array contains a bunch of strings, numbers, or awesome flying pigs, you're still going to write that exact same for
loop for the eleventy billionth time. Same goes for the error checks: you will need to check what that flaky I/O function returned every single time you call it lest you accidentally end up in an invalid state and open a wormhole to the eaty pig dimension or something.3 Way to go, you just got us all eaten by forgetting to detect an error condition. I hope you're proud of yourself!
Eaty pigs aside, here's the problem we have: The code that doesn't care about the value(s) is intertwined with the code that does, and all this has to happen in the proper order otherwise, well, hello pig dimension. So we can't just tease it apart, can we? Actually, it turns out we can. But we're going to have to work up to it.
At this point everyone usually breaks out a box metaphor. It's serviceable as far as mnemonic devices go, but it's not really that great for introducing the theory (more on that later) so let's try a different approach. Let's suppose we have this simple function:
function square(x) {
return x * x;
}
We call it with some number, it gives us back that number raised to the second power. Easy, right? So now we have our function, and we can use it to compute the square of any numeric value that happens to be lying around:
let x = 812;
x = square(x); // x = 659344
...and we can use the same function to square every value in some list:
for (let i = 0; i < numbers.length; ++i) {
numbers[i] = square(numbers[i]);
}
We can even use it with a value that might not exist, as long as we've checked that it does exist first:
if (value !== null) {
value = square(value);
// maybe do other stuff with it here
}
So the values themselves always work the same way, but we're using them in different contexts. The value is inherently entangled with its context, but our function doesn't care about the context, so we have to disentangle it first. After all, it's our responsibility to call the function, right? It seems we're doomed to write the same code over and over and over again... for the rest of eternity...
You're probably familiar with the concept of callback functions. The idea here is that we give the browser (or whatever) a function, and it arranges to call us back through that function whenever something stupid interesting happens. This saves a ton of work, as we'd otherwise have to watch for events ourselves so we can run the proper code when they came in. Of course this is just a convenience, but it should give us a hint: It doesn't have to be our responsibility to call a function! We can always give a function to someone else, and they can call it for us when the time is right.
Here's a functor:
function nullAware(func)
{
// note: this returns another function!
return function (value) {
if (value !== null) // look familiar?
return func(value);
else
return null;
};
}
A functor is like a "pipe fitting" for functions. What it does is to take some function, like our square
function above, and make it work for values in a specific context (we'll call these "entangled values" from here on out), without the need to do anything meaningfully different at the call site compared to a standard function call.
let tryToSquare = nullAware(square); // remember, our functor returns a function
console.log(tryToSquare(8)); // 64
console.log(tryToSquare(null)); // null
So now we have a way to call functions within the context of "the input value might not exist", without the need to check whether it exists ourselves. We can even chain these together just like we can chain together regular functions (i.e. passing the output of one as the input of the next), and the whole thing will automatically short-circuit the moment a single null
is encountered, because the functor has handled that for us. And we never even had to check for null
once!
If you're a JavaScript developer, there's another functor you may be familiar with which is built right into the language: Array.prototype.map
. It works as a "pipe fitting" in the same way as our nullAware
functor above, taking a normal function and applying it to every element in the array for us. There's a subtle difference from nullAware
in that calling .map()
automatically does its thing and returns a result (eager evaluation), but the underlying concept is nonetheless the same:
let cube = x => x * x * x; // ES6+ arrow function, just because I can
let list = [ 1, 2, 3, 4, 5 ]; // put some numbers into an array
let cubeList = list.map(cube); // look ma, no loops!
console.log(cubeList); // [ 1, 8, 27, 64, 125 ]
So this provides us with a way to call functions within the context of "there are actually multiple input values and they are part of a list". And since we can always delegate by passing a function off to someone else, it's always possible to write a functor like this, regardless of what the context is, and we never have to navigate that context ourselves again!
On a side note: This whole "context" business is all pretty abstract, isn't it? Yeah, that's why the box metaphor is useless as a teaching tool. We don't actually have things in neat little boxes yet, which is the problem we're trying to solve in the first place! (even if we don't realize it yet)
A well-behaved functor must follow a few rules. There are rigorous mathematical definitions of these rules, but here they are in plain English:
- A functor supplied with an identity function
x => x
(i.e. a function which outputs exactly what was given as input) must remain an identity function within the context of the functor. For example,array.map(x => x)
always returns an array containing the same values as the original array. - Two applications of the same functor with different functions, when chained together from outside, must produce the same output as chaining those same functions from inside. In other words,
array.map(square).map(cube)
is exactly equivalent toarray.map(x => cube(square(x)))
.
Or to put it even more simply: It doesn't matter whether we call the target function(s) on the values ourselves (assuming we properly navigated the context), or if the functor calls it for us. The outcome is the same.
So a functor lets us take a context-free function and turn it into one which is context-aware. In more technical terms, we're said to have mapped the function over the context; from here on I will refer to this operation simply as map
. In this way, we no longer have to manually write two (or more!) versions of each function we want to use this way. This already gives us a great deal of power, but what if the function we want to map itself returns an entangled value? Here are some examples:
- Two or more I/O functions used in sequence, any of which may fail and return
null
to indicate their failure, instead of a meaningful value. How dare they! - Replacing each value in an array with multiple values, which requires the mapping function to return an array (as a function can only have a single return value).
- A value you don't actually have access to right now (e.g. JavaScript
Promise
), which forces every further computation to be deferred and therefore you can never escape thePromise
context--every subsequent step in the chain must also be represented by aPromise
(note:Promise
is itself very monad-like, though it does break the rules in the name of convenience; we'll see this later).
If we use a standard functor in situations like this, we usually end up with values which are doubly entangled. For example, mapping each item of an array to two items by returning an array from the map function will produce an array of arrays. This is not usually what we want. To solve this problem, what we need is an operation, let's call it flatten
, that disentangles the value(s) from the "inner" context after a map and puts them back into the "outer" one. In most cases map
and flatten
are combined into a single operation, typically called flatMap
or bind
, though it can have different names depending on the specific context. For example, JavaScript's Promise
has .then
.
It turns out that, if we have the ability to both flatten
and map
over some context, we're already at least two thirds of the way there to having a monad. A monad is an abstract mathematical concept with some scary-looking rules and identities, but in plain English it consists of three basic components:
- The ability to plant a value into some context: in other words, to create an entangled value at will. For example, we can readily stuff something into a new array, or set some variable to
null
with the understanding it'll be checked for later. This is a basic operation of monads and is sometimes called weird things likepure
,return
(no relation to the return values of functions!), orunit
. We'll call itentangle
. - A
flatten
operation over the context to resolve double entanglement, as described above. - A special functor, we'll call it
thru
4, to map functions over that context, with two additional caveats: 1. The mapped function should only return similiarly entangled values (we'll see why this is in a bit), and 2. The functor mustflatten
the context after the mapping operation is complete.
An important thing to note here that is left out of basically all monad tutorials is that none of this says anything about the structure of your program. It is too abstract a concept to do so. If you have the three things above, you already have a monad. The functor and flatten
operation don't even have to be concrete things: they can merely exist "in spirit", for example as the general form of those for
loops you keep writing!5 All your programs are already lousy with monads and you just didn't know it; they've been hiding in plain sight the whole time! It's all a bunch of abstract nonsense, but useful abstract nonsense because recognizing that this pattern exists will allow us to formalize it into something more concrete.
As with functors above, there are a few laws a monad must follow to actually qualify as one, mathematically speaking:
- Creating an entangled value and using the special monadic functor (
thru
, if you've forgotten) to map some function over it is exactly the same as calling that function on the value directly. This is called the left identity:(entangle v) thru func === func(v)
- Using
thru
to mapentangle
over some existing context/value entanglement ([c+v]
) always produces the original value(s) entangled in the same way. This is called the right identity:[c+v] thru entangle === [c+v]
- Chaining
thru
from outside is the same as chaining from inside. In other words, the associative property:[c+v] thru func1 thru func2 === [c+v] thru (v => func1(v) thru func2)
. Note that this depends on the left identity law being true, so if that law is broken, this will be too.
Promise
breaks both Rules 1 and 3, because the value(s) used as input to a monadic context may themselves be entangled (this is another thing monad tutorials don't tell you!) and the language deliberately prevents you from creating a doubly-entangled promise value (i.e. a promise for a promise). This is a fair tradeoff in the name of convenience, I think: a promise for a promise is kind of a cruel joke, as it--at best--represents a scheduling conflict--"I'm booked right now, but sign up to be notified when I have an opening and then I can schedule you to get the value." But it does make Promise
explicitly not a monad (in the mathematical sense), so this is something to keep in mind.
Incidentally, our example "variable that may be null" context is not a monad either, and breaks the rules in the same way Promise
does. Can you guess why?
🎶 Jeopardy theme song plays 🎶
Give up? It's because there is only one null
value! [value or null] or null
can't be encoded this way, and therefore both Rules 1 and 3 are broken for the set of null
values (i.e. null
itself).
So by now you've probably figured out what functors buy us (note that a monad is just a special case of functor): they let us consider the combination of a context with the value(s) it contains as a single atomic value in its own right. At least, we can do this as a mental exercise. That's valuable, but can we take advantage of this in our code? Yes, we can!
Remember how I said the box metaphor was an awful teaching tool for introducing the concept of monads, but made a good mnemonic device? Yeah, well, now that we know what defines an (abstract) monad and can recognize the distinction between values and the context they exist within, we can define our own interchangeable "smart boxes" to encapsulate those contexts! Such a "smart box" might consist of the following standard components (assuming JavaScript class
syntax):
- A way to get a value (or another box!) into a brand-new box. Since we'll be using the
class
syntax, we can do this in the constructor. This will be the concrete realization of our monad'sentangle
operator (see above if you've forgotten). - A
thru
instance method defined on the box class to map functions over the box that themselves return values in the same type of box, allowing us to chain usages ofthru
. This, as it turns out, represents the monad'sthru
operator. Will wonders never cease? - Optional
map
andflat
methods, if it makes sense to use these with our type of box. Mathematically, a monad is a kind of functor so it must support bothmap
andflatten
, but the mathematical definition only covers the abstract context itself; we don't have to split them up if it doesn't make sense for our program to use them separately. That's the beauty of it all: we're not creating monads here, we're just taking advantage of the fact that they exist!
For our first example, we have here a class representing a context that always contains exactly one value:
class Box
{
constructor(value) {
// box it up! (this is our `entangle`)
this.value = value;
}
flat() {
// note: mathematically, `flatten` is undefined if the boxed value isn't another box.
// (because we must return exactly one value but there might be zero or more!)
if (this.value instanceof Box)
return this.value;
else
throw TypeError("It's already flat!");
}
map(func) {
// `map` is our functor, it's like the CPU of our smart box!
return new Box(func(this.value));
}
thru(func) {
// per the left identity, we *should* just be able to call `func()` directly and be done
// with it, but then we'd need static typing to stop evil box-unpackers!
return this.map(func).flat();
}
}
So now we have our box, and with it, can do things like:
let stupidBox = new Box(812);
stupidBox.map(x => console.log(x)); // prints 812 to the console
// (and also produces a box containing `undefined` but we're litterbugs so we discard it)
But this is kind of a stupid box, isn't it? By putting anything in this box, all we've done is to take a completely context-free value and entangle it with a context whose functor just does what we can already do ourselves with equally many lines of code (i.e. one). This is what makes the box metaphor so useless for introducing the concepts; to wit, why would I make things harder for myself by putting a thing in a box? Of course, now we know that the boxes (contexts) already existed--we just weren't able to recognize them until now.
Furthermore--and I really want to stress this now, before we go any further down the monadic rabbit hole--this class gains absolutely nothing by being monadic. Because the box always contains exactly one value and carries no additional information, literally everything meaningful we can do with thru
can be done just as well with map
. I'm not confident we have enough context for me to explain this adequately right now; however, it should become clearer with the next two examples.
So all told, this first attempt isn't very practical--but it does show us a basic design for a "smart box", and now we can use this outline to make more useful boxes. If we follow this design to encapsulate different contexts, we'll know exactly what to do when receiving any kind of box we want to operate on as if it were a single unit--just call its .thru()
or .map()
method and supply the appropriate function!
Remember how I said that "variable that may or may not be null
" is not a proper monad? Let's make a version that is. An operation that produces a Maybe
box like the one I'm about to show you is saying that it may either succeed and return some value, or fail and return nothing (because it failed--there's nothing to return).
class Maybe
{
constructor(haveValue, value) {
// `value` is ignored if `haveValue` is false
this.haveValue = haveValue;
this.value = value;
}
flat() {
if (this.haveValue && this.value instanceof Maybe)
return this.value;
else
throw TypeError("It's already flat!");
}
map(func) {
if (this.haveValue)
return new Maybe(true, func(this.value));
else
return new Maybe(false);
}
thru(func) {
// remember: `thru` is just "map then flatten"
return this.map(func).flat();
}
}
Unlike our possibly null
variable, this class represents a proper monadic context because we can readily nest Maybe
objects. The ability to nest a context within the same context might sound useless--and in a real program it often is (see Promise
)--but by definition, thru
is the same as map
followed by flatten
, and flatten
isn't defined for already-flat contexts (it's kind of like dividing by zero). So we must have the ability to encode double entanglement or we don't really have a monad.
So anyway, just like our Box
class from before, with this one we can do the same kind of stuff:
let lessStupidBox = new Maybe(true, 8); // it starts out containing 8
lessStupidBox = lessStupidBox.map(x => x * 2); // now it contains 16
lessStupidBox = lessStupidBox.map(x => x * 3); // now it contains 48
And now we can map our regular old functions over the Maybe
box without having to manually disentangle the value from its context of "it possibly doesn't exist". Not a single if
statement in sight! Very convenient! So here's a question: What would it mean to map a function over a context that doesn't contain anything? Hmm... well, if you put nothing into a pipeline, you get nothing out, so mapping any function over an empty Maybe
box should just result in the function not being called, no harm done, and you get the empty box back:
let nil = new Maybe(false); // whoops, something stupid happened, here's an empty box
nil = nil.map(x => x * 3); // function not called, still an empty box
Our program doesn't crash, doesn't even produce garbage data (such as NaN
values), and there's not a single null
check to be seen for miles. The functor encapsulated by the Maybe
class handled it all for us!
So you may have spotted a problem here: if Maybe
is considered only as a standard functor context (i.e. one in which the only available operator is map
) containing some value, then there'd be no way for us to remove that value; we can only transform it. We can't turn a "something" into a "nothing", or in more general terms, we can't change the shape of the box. So if we wanted to chain some Maybe
-returning operations together with the convention that the first empty Maybe
means failure, we couldn't actually do that because the Maybe
box, once initialized, will always have something in it.
Or can we? As it turns out, thanks to the entangle
and flatten
operators of the monad, we can! Behold:
// start with a new `Maybe` box containing 9000.
let box = new Maybe(true, 9000);
box = box.map(x => x + 1);
// now it's over 9000...
// reminder (again): `thru` is just "map then flatten"
box = box.thru(() => new Maybe(false));
// ...and now it's empty!
Keep in mind that a monadic box, once emptied, will always be empty (unless we go off the rails and mutate the box ourselves), but this is just a mathematical truth we have to deal with. Mapping a function over nothing always produces nothing, in the same way that a long sequential chain of multiplications, once it encounters a single zero, is doomed to produce zero as its result.6 In practice this isn't a problem when working with monads, because we can always use entangle
to put something in a new box and start over.
So now that we've learned what a monad is and implemented both Box
and Maybe
monadic classes ourselves, let's step back for a minute. Monad tutorials in general tend to lead off with a Maybe
as their first exercise; I chose to lead instead with an obviously useless box. There's a good reason for this: to a seasoned imperative or OOP programmer, Maybe
tends to look like a pointless abstraction, as while you save a line of code for every null check, it tends to add runtime overhead in the form of extra function calls for computations you would normally just do inline (I don't think functional programmers fully understand this--and why should they when functions, not statements and expressions, are the fundamental building blocks in their world--but it was a pretty big mental block for me at least). In my experience, the whole exercise ends up being skimmed over and then, because the tutorials rarely go into the theory to any meaningful depth, there's no intuition to build off of for understanding the more complex examples. Starting with an openly and deliberately worthless example gets rid of the distraction to focus only on the structure of the thing.
This is also why I chose to give an example of a functor earlier on in the form of Array.prototype.map
, because this is a more practical application of the concept and is built right into the language. Developers who've used JavaScript for any significant length of time do tend to understand what map
does and why it's useful, so it's a great jumping-off point.
Of course, I'm assuming here. Hopefully I'm right about all this and you've been able to follow me this far! The Great Curse is a tough one, but hopefully I've managed to overcome it. Anyway, back to our boxes!
So now we've made a useless Box
class, and a rather less useless Maybe
class. Both of those were pretty boring though, so let's make something more interesting.
Maybe
as a concrete representation of potential failure has this small issue that, if an error occurs at any point, we only get an empty box as output. We can't do anything with an empty box (unless, again, we cheat and poke at the Maybe
object manually), so the process may silently abort midstream and we won't even know it. How can we solve this? Checking the return value of the individual functions manually and using throw
doesn't count--we want to build on what we already have with Maybe
.
Introducing Either
, a type of box which always contains a single value and can be either Red (to indicate an error) or Green (to indicate success). This box encapsulates the abstract concept of "some part of the process may have failed and, if so, it aborted and produced an error value" (that was a mouthful, wasn't it?). Here it is in all its glory:
class Either
{
constructor(isGreen, value) {
this.isGreen = isGreen;
this.value = value;
}
catch(func) {
if (!this.isGreen) // box is red, map over it (actually `thru`)
return func(this.value);
else // green box, no error, pass on the value
return new Either(true, this.value);
}
flat() {
if (this.value instanceof Either)
return this.value;
else
throw TypeError("It's already flat!");
}
map(func) {
if (this.isGreen) // box is green, map over it
return new Either(true, func(this.value));
else // red box, just pass on the error value
return new Either(false, this.value);
}
}
thru(func) {
// third verse, same as the first.
return this.map(func).flat();
}
}
This works very much like our Maybe
above, but there's two important new wrinkles to note: 1. map
, when applied over a red box, simply passes on the error value without calling the mapped function (leaving the box and its color unchanged); and 2. There's a brand-new catch
method! catch
works like thru
, but only calls the mapped function if the box is red. If it's green, it passes on the value--in other words, the exact opposite of thru
. It's like having two monads in one!7
Also similarly to Maybe
, if all we use is map
, we can only change the value stored in the box, but not the box's color. If we need to change a green box to red, we have to use thru
in combination with new Either()
. So now we can do stuff like this:
openFile("file.txt")
.thru(file => readLine())
.map(line => console.log(line))
.catch(error => console.log(`Oops! ${error}`));
Because red boxes simply have their error value passed through without applying the function, if either openFile
or readLine
fail, then the box turns red, the operation automatically aborts and the catch
handler gets called at the end of the chain. Then you can find out exactly what went wrong. See how flexible these things are?
Monadic boxes like these can do all kinds of great things, but the basic underlying mechanics are always the same: you can either map over the box with map
to operate on its current contents, or, if you need to create a new box in the process, use thru
. That's all there is to it! If you know the value you have is a monadic box, regardless of which kind of box it is, you always know exactly what to do with it! It's an amazing abstraction to have under your belt.
As an aside: You may have noticed that the way this pair of .thru()
and .catch()
methods works is very similar to the pair of .then()
and .catch()
provided by JavaScript for Promise
objects. This is no coincidence; while Promise
breaks the rules and therefore disqualifies itself from being a true monad, it's built on the exact same concepts as our Either
monad here. Something to think about!
Now that we've gotten this far, I wanted to point out something interesting before I close out the tutorial. I've used the term "context-free" a few times to refer to values which are not entangled with any context--meaning you can pass them directly to one of your functions without needing to do anything else before you use the value. It's a value with no strings attached, right? Want to hear something wild? This state of being context-free is itself a monadic context!8 The entire value space can be thought of as a kind of "null context" in which entangle
is the identity function, map
is the identity functor (i.e. it simply calls the mapped function with the value), and flatten
is a no-op (no nesting, infinite nesting, it's all the same).
We can even do something really crazy and turn literally the entire value space (except undefined
and null
for reasons) into one of our monadic smart boxes:
// trained professional on a closed course. do not attempt!
Object.prototype.flat = function () { return this; };
Object.prototype.map = function (func) { return func(this); };
Object.prototype.thru = function (func) { return this.map(func).flat(); };
// no implementation for `entangle` because:
// 1. it's an identity function
// 2. there's no good place to put it
// 3. we can just poof up an *ahem* "entangled" value anyway
console.log((812).thru(x => x * 2)); // 1624
Okay, so maybe I'm stretching the definition of "smart" to its breaking point but... pretty crazy, huh? You can even plug in all the monad laws and it'll all check out!
So now, hopefully, the Great Curse has been well and truly broken and you fully understand both what a monad is in the abstract sense (having been hiding in plain sight all along!), as well as how to design concrete types of monadic boxes that take advantage of their existence. Values are intrinsically entangled with the contexts they exist within, that's true, but there's no reason you can't make the context itself do the work of disentangling them for you. Nobody really wants to write the same few lines of code over and over and over again, and now, thanks to monads, you don't always have to! Have fun in your new monadic adventures!
But, um... Maybe be careful of opening that pig dimension? Nothing good can come of that, I assure you. 🐗
Footnotes
-
This famous quote is originally from http://james-iry.blogspot.com/2009/05/brief-incomplete-and-mostly-wrong.html, fictionally attributed to Philip Wadler. It was meant as a joke: the joke being that it's 100% mathematically accurate, but also 100% useless for actually teaching anyone about monads. ↩
-
Oh sure, people think an elephant in the room would make a great conversation starter. Then you accidentally offend it and get trampled to death. Not so fun to talk about anymore, is it? 🐘 ↩
-
I believe this tends to be referred to in stuffed-shirt circles as "undefined behavior". Trust me, it's always pigs. Every single time. ↩
-
I had a really hard time choosing a non-scary name for this one that could apply more or less equally to all kinds of monads. A lot of people go for
chain
, but what would it mean tochain
lists, for example? That just sounds like concatenation, which is misleading.thru
is better mnemonically, as it directly communicates our intent: rather than mapping over the monadic context, we're mapping through it, creating a new context in the process. ↩ -
In exactly the same sense that you could write a
for
loop that addsy
to itselfx
times; in the end you've still multipliedx * y
, you just made it harder for yourself (and the computer!). ↩ -
Unless there's also an ∞ Infinity involved. Then it gets complicated. ↩
-
This construct, besides being a monad, is also sometimes called a bifunctor, as there are two separate "compartments" and each one can be mapped over independently. That said, the compartments of
Either
are not completely independent; the box is only ever red or green--never both. It's entirely possible to create a version that does accept two values, but that's left as an exercise for the reader. ↩ -
It also happens to be a comonadic context, but let's not go down that rabbit hole! 🐇 ↩
One thing I found with your
Maybe
implementation: I cannot chain a.thru()
onto an emptyMaybe
.Surely this should be possible?
Example:
(This is because the function passed to
.thru()
returns aMaybe
, but.map()
may or may not use that function and so its result could be one or two layers ofMaybe
s;.flat()
then fails if it is only one layer.)The implementation of
.join()
(flatten) in Dr. Frisby's Mostly Adequate Guide returns an emptyMaybe
from flat if the targetMaybe
is empty:In your
Maybe
(I think this is correct):I do not know if this is mathematically correct, of course.
The one from The Guide would return an unwrapped value if called on a non-nested
Maybe
. You just have to use it safely; of course, that applies to both, as your implementation throws an error.My version of your implementation would return an empty
Maybe
.I would dearly like it if you continued on to
ap
andliftA2
...And maybe pointfree as a bonus...
Aside:
The names I have often seen for constructing a Monad are
point
andof
.