Skip to content

Instantly share code, notes, and snippets.

@kvverti
Last active October 15, 2023 23:57
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kvverti/2ea422007a0f18b7877326cf3f8def86 to your computer and use it in GitHub Desktop.
Save kvverti/2ea422007a0f18b7877326cf3f8def86 to your computer and use it in GitHub Desktop.
What the heck are monads anyway?

What the Heck is a Monad, Anyway?

Querying one's favorite search engine with the text "a monad is" yields this answer on StackOverflow, which explains, in excruciating mathematical symbology, what a monad in fact is. But, don't worry. Understanding monads doesn't require you to learn the nuances of such concepts as moirails and eigenfunctors. I would suggest that the concept of monads can be distilled into a clearer single sentence.

A monad is a control abstraction that defines a composition of effectful functions.

Let's unpack.

Part 1: Control Abstractions

In the practice of computer programming, control logic refers to program code that defines the infrastructure needed to support a particular behavior. Broadly, control logic is the cruft and/or boilerplate required to execute the code we're actually interested in writing. Control logic is a broad class of code, and includes such mundane patterns as:

  • Error propagation
  • Iteration
  • Stack manipualation
  • Virtual dispatch

A commonly written example of control logic is the classic error propagation routine.

int err = func_which_may_error(&output_variable);
// this is control logic!
if(err) {
  return ERR_FUNC_DID_ERROR;
}
use(output_variable);

A control abstraction provides a way to separate a control structure from the interesting code inside it. Common control abstractions are:

  • Exception handling, for error propagation
  • Objects and interfaces, for virtual dispatch
  • Function invocation, for stack manipulation
  • Memory management

The first two (exceptions and objects) are commonplace in languages that prefer or enforce the object-oriented paradigm. Function invocation is much more fundamental and is present in almost every language, excepting some of the more limited assembly languages such as Malbolge.

The presence of many control abstractions is one of, if not the, defining feature of "high level" languages. High level systems languages like C++ and Rust provide destructors, automatic resource management, and smart pointers. Using these control abstractions certainly results in less, more interesting code than if the control logic were to be entangled with the more unique parts of the program.

Part 2: Effectful Functions

The term effect can mean many things in a programming context, but I use it here to mean "anything that results from a function other than its return value." Effects generally include:

  • Returning an error
  • Throwing an exception
  • Using file IO or nondeterministic external state
  • Allocating or deallocating resources
  • Spawning child processes
  • Synchronizing in a multithreaded environment
  • Reading or modifying global variables

An effectful function, therefore, is a function which produces or consumes at least one effect. In most languages, any function can be an effectful function, but some mainstream languages restrict some effects to specially decorated functions. For example, Java has checked exceptions which must be opted into with a function throws clause, a widely criticized form of effect. C++ has a similar, but negated version of this with noexcept functions, which explicitly opt out of throwing exceptions.

Part 3: Function Composition

Function composition is a way to sequence functions by feeding the output of one function into the input of another function. Those with a US elementary school education may remember the mathematical definition of function composition:

(f ∘ g)(x) = f(g(x))

Function composition in the functional paradigm allows the programmer to express a sequence of computations - functions - as a single computation that chains each component function one after the other, output-to-input. Ideally, if f and g produce an effect, then the composition of the two should produce that effect as well. However, plain function composition requires that the result type of g match the argument type of f exactly. The functions can no longer be composed unless f accepts the exact effect! Finally, this is where monads enter the picture.

Part 4: Monads

A monad, fundamentally, defines how functions are composed in the presence of an associated effect. A monad is made up of two operators, often called unit and bind, and often given in Haskell syntax like the following, given a monad m.

unit :: a -> m a
bind :: (a -> m b) -> (m a -> m b)

For those unfamiliar with Haskell syntax, the type t1 -> t2 is a function type, which accepts an argument t1 and returns an argument t2. And, the construction m t1 refers to the type t1 combined with the effect m.

The unit operator takes a plain value and produces that same value, but within the associated effect. And, importantly, the bind operator takes an effectful function and produces an equivalent effectful function that accepts the associated effect as an argument. With bind, one can compose two effectful functions by f and g by simply using plain function composition on bind f and g!

Part 4.1: What is Bind, Exactly?

Monadic bind has several well-known implementations for specific effects. For lists, streams, and iterators (yes, list is a monad, bind is better known as flatMap (or, if you're a C# programmer, selectMany); and for optionals and futures it is better known as andThen or then. Often, bind implementations in object oriented languages will be defined as instance methods of the class that represents the associated effect.

The bind function can be seen in two complementary ways. One way to describe bind is that it "unwraps" a value out of the effect and runs that value through a function. The other way is that it transforms a value inside the effect, then flattens the nested effect. The advantage of the second explanation is that it can be used for monadic effects that resist being modeled as simple data types (like the State monad instance).

An important thing to note is that by allowing the composition of effectful functions, bind allows the result of an effectful function to depend directly on the result of another effectful function. Haskell's do notation, Scala's for comprehension, and the async-await syntax of Rust and C# are all examples of bind being used to chain effectful expressions one after another.

Part 5: Epilogue

So, now you know what monads are for. Each monad instance defines how to route the control logic for a certain effect in such a way that functions producing that effect can be composed. But, monads only deal with one effect at a time. How does one compose functions that have more than one effect? How does one compose effects?

Consider an iterator. An iterator follows the rules of the list monad, because you can (in principle) collect the elements of an iterator into a list. But what if your iterator doesn't yield values directly, but instead yields a value in an effect? In most programming languages, there is no way to use regular iteration to perform some effect. Instead, there are separate constructs in libraries for fallible iterators and asyncronous streams. As more and more effects are added into languages as first class features, the problem only gets worse.

Fortunately, there is a way to unify these disparate interfaces and define a way to compose one effect with another. Up next time, the monad transformer.

Part 6: Reference and Further Reading

Many thanks to Haskell for repeatedly breaking my brain and to Rust for making it whole again. Here are some other people's writings on monads.

Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License.

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