Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@tef

tef/monad.md Secret

Created July 29, 2019 04:51
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tef/0e3676750185c2c3d2082f22722cc380 to your computer and use it in GitHub Desktop.
Save tef/0e3676750185c2c3d2082f22722cc380 to your computer and use it in GitHub Desktop.

What is a monad?

Ignore functional programming for a moment. Well, ignore any functional programmer who doesn't have a soft spot for users.

Instead, Imagine an object

  • That wraps a value
  • The value can only be accessed by passing a callback
  • The callback should return a new wrapped value

You will have to write a lot of nested callbacks to use it, but on the other hand you can pick and choose which callback to run. You could even run callbacks more than once.

A monad is a way of bolting together callbacks using a wrapped type, that defines how the callbacks get invoked. Is that it? Almost.

With sufficient sugar, you can transform a more imperative program into callback soup automatically, and as a result, stuff like writing async code can become almost pleasant.

That's also what a monad is about. Sort of.

Something like this:

get_future().then(function (x) { return Future(x+2) })

Can be written like this:

async {
	x = await get_future()
	return x+y
}

In OO languages, you might call it something like a Future, a Builder, or an Observer, or a mess of callbacks and method chaining, depending on how you compose them. In functional languages, they tend to use more mathematical sounding terms.

Don't let me put you off category theory, but it isn't that helpful right this moment. A Builder pattern doesn't lay bricks, an Observer doesn't look at the stars, don't worry too much about if your monad isn't simply a monoid in the category of endofunctors.

The important point? When you use callback soup, you can change the evaluation order of a program, assuming your callbacks don't mind the shenanigans. If you use a monad, you might not make so much of a mess.

Why are monads important?

Explaining what a monad is, like reversing a list on a whiteboard, is a performative exercise. By comparison, no-one gets asked about co-monads, despite being far more ubiquitous. Monads are only as important as they make a programmer feel.

In other words, they are important, but not as much as you might think. Unless you're thinking about Haskell. Monads are pretty useful in Haskell.

A pure, functional, lazy language, Haskell lets you write programs, and then evaluate only the parts that you need. The problem starts when you want to define an evaluation order, or handle IO, and you don't want to give up type safety. In other words, "open file a, then open file b" is one of the harder programs to write in Haskell.

People yell about monads because they make something that seemed impossible, tractable, in a language that went out of its way to prevent it from ever happening. That said, Haskell isn't the only language that benefits from monads, or the notion of controlling execution order and composable chunks of code.

Sadly yelling "monad laws" at strangers on twitter has proved to be a very unsuccessful means of communicating this idea. Aside: Please, please stop yelling about the "monad laws", it literally doesn't matter in any language outside of Haskell. The rest of us have no referential integrity. Please. Just. Stop.

In practice, other languages might use async and Future instead of do-notation and Monads. It doesn't matter what you call it, as long as you can clobber code together without having to manage the callback soup by hand, and can plug in something else to change the rules.

Without sugar, a monad isn't much more a wrapper for a value, that defines and enforces the evaluation order of the callbacks passed to it. With sugar, you can transform more regular looking code into callback soup, and you can plug in a monad to determine how things get run.

The big idea is composition, yes, but also transforming your code into a more flexible form to allow changing the evaluation order.

The big idea is program transformation

Let's ignore monads, and focus on something a little simpler. Push and Pull. Take a function, a(), that returns a value, and a program that prints the output:

x = a()
print(x)

Turn the function inside out.

a(function (x) { print(x) })

In one, we pull a result out of functions, and in another, we push them inside. The technical name for turning a function inside out is "continuation passing style", and as do-notation, async/await shows us, we can rely on the compiler to handle it for us, rather than managing the control flow by hand.

In other words, given code that works on pulling out a value, we can transform it into code that takes a value given to it.

It's a little similar to how iterators can be pull based (also called enumerator), or push based in some languages:

iterator = collecton.iter()
while iterator.next() {
	print(iterator.value)
}

collection.forEach({
	function (value) {
		print(value)
	}
})

Although not entirely. We haven't handled errors in the push based one, nor do we handle the end of sequence. The true inversion of an enumerator is an observer.

enumerator = collecton.iter()
while enumerator.next() {
	print(enumerator.value)
}
handle = collection.subscribe({
	on_next: function (value) {
		print(value)
	},
	on_err: function (exception) {
		// do nothing
	 },
	on_end: function () {
	},
})

# wait for events

handle.expire()

Any function that takes an enumerator can be transformed into an observer that's given the values. This might not seem like much, but in C#, almost any operation you can do on an enumerator, you can also do on an observer.

handle = events.filter(myEvents).subscribe(myCallback)

Remember how a monad was a wrapped object, accessible by callback, that you can compose? An observer is very, very close to that idea. The idea of method chaining up filters is quite close too, but not exactly.

It's close enough.

You can write pull based code, and get push based code, then decide the exact order of operations. You can take things that work on enumerators, wrap them, and make them work on observers. Composition, and Transformation too.

When you add in async, await, it really does start to resemble do-notation . In truth, we already had monads in OO languages, but it wasn't until async and await that we got excited to use them.

When you do it in C#, you call it LINQ, when you do it in F# you call it Computation Expressions, when you do it in Haskell, you call it a Monad, and when you do it in JavaScript, everyone tells you you're doing it wrong.

Still, I haven't really described a monad in full. I haven't talked about error handling much. Although values get pushed into callbacks, errors still bubble up. This, amongst other reasons are why monads are not the be-and-end-all of design patterns, and why things are a little trickier to talk about outside of Haskell.

Especially since we focus so much on the callbacks themselves.

In a true haskell monad, the callback must return a wrapped type. People wax endlessly about the Haskell rules for composition without explaining why you'd ever want to do such a thing in the first place or what you'd get from doing it.

In other languages, you can give less of a shit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment