Modern Computing

In recent decades, horizontal scaling, i.e., adding many machines to a cluster rather than fortifying a single node, has proven to be the most enduring form of scaling. Leveraging commodity hardware has prevailed as a fault tolerance, sustainable, and cost-effective method for yielding computation. The shift to horizontal scaling didn’t come without its tradeoffs.

A primary shortcoming of horizontal scaling is the coordination headache represented in network communication, a well-known I/O bound process that stifles the whole system if not handled correctly.

When the processor waits for an external event, be it a keystroke or the network packet, it wastes billions of cycles busy waiting. This blocking behavior of the slow I/O processes is an antipattern. Optimally, the CPU should always be running instructions, i.e., its utilization should be high, but in this case, it’s low, effectively doing nothing.

New computational models had to evolve to cope with the blocking behavior of the slow I/O processes. These models have involved primitives like threads, tasks, and coroutines. In the next section, we will look at how Javascript manages concurrency in its single-threaded environment and how the event loop attempts to maximize a processor’s utilization.

The Event Loop and Continuation-Passing Style

Javascript is a single-threaded asynchronous language, i.e., the runtime doesn’t wait for the execution of a task unless the programmer explicitly asks it to wait. This design has helped the language to be extensively used regardless of how slow and blocking I/O operations are.

The Javascript runtime consists of four components: call stack, heap, message (task) queue, and event loop. The stack contains the function calls, and the heap represents unstructured slow memory for allocating objects. The message queue stores the unblocked functions that are ready to be processed. Finally, the event loop orchestrates the show by continuously monitoring the call stack and the message queue. Whenever the call stack is empty, the event loop submits the next task waiting to be processed from the message queue and dispatches it to the call stack.

Philip Roberts provides more detail on the process in his talk at JSConf EU, aptly titled “What the heck is the event loop anyway.” You can also run your JS code and watch how the event loop and callback queue work using loupe, an online tool that Roberts created to visualize how the runtime works.

The continuation-passing style has become prevalent in many programming languages, not just Javascript. One of the reasons for its popularity is that it deconstructs asynchronous code. It doesn’t matter if the code runs on one or multiple machines or if the code is single- or multi-threaded; asynchronous programming will increase utilization either way.

Uncertainty plagues continuation-oriented codebase, which has led to the emergence of patterns like callback hell, chaining promises, and async-await. Although some patterns are less error-prone and more familiar to programmers than others, they all require rigorous testing and error handling, which tends to clutter the codebase and overwhelm developers.

Error Handling

As computational models evolved over time to incorporate continuation and threading, error handling had to evolve as well in order to account for this increased complexity. Programming is ultimately about maneuvering nondeterminism: finding ways to process the happy path while gracefully handling the unhappy path. Although they don’t receive equal attention, both are equally important for maintaining healthy and readable software.

There are several mechanisms to handle errors: returning nullable objects, throwing exceptions, or using monads. Returning nullable objects is the least favored way of handling failures because this mechanism lacks transparency as to why failures happened. Tony Hoare once apologized for creating the null reference, calling it a billion-dollar mistake at a conference in 2009. Since null pointers lead to innumerable errors, vulnerabilities, and system crashes. In business terms, it causes many websites to be down and services to stop responding.

Throwing exceptions is generally more favorably looked upon than returning nullables, but this mechanism complicates the code. Exceptions are expensive to create since they take a snapshot of the call stack. They also break referential transparency, i.e., safely replacing that piece of code with the value it computes and vice-versa. The callee has to handle different exceptions in a customized way and repeatedly check for the exceptions they might have missed since any exception could be thrown at runtime.

Handling exceptions becomes even more complicated when we introduce threading and asynchronous code since exceptions are missed if they are thrown on non-awaited async methods, although static analysis may sometimes catch this. The callee also has to manage aggregate exceptions in the event that many exceptions are thrown from concurrent or parallel code.

Monads

A monad is a powerful design pattern that was developed to handle errors and uncertainty. When monads are used, a function’s definition includes strongly typed errors. This makes the error handling code explicit to the reader and, therefore, easier to reason about. As a result, the function includes compile-time error checking, which reduces failures because relying on a compiler is preferable to relying on a programmer to handle each and every exception since automation always has a better chance of eliminating human errors.

The origin of monads is in category theory, a general theory of mathematical structures that most programmers aren’t familiar with. Many programmers fall prey to the monad fallacy, which presupposes that once a person understands what a monad is, they lose their ability to explain it to others.

Every monad has three parts: a type constructor, a type converter, and a combinator. Below are abbreviated explanations for each. See the appendix for formal definitions.

  • A type constructor is a wrapper class that encloses a pure value, be it a primitive type or an object.
  • A type converter is a bridge that converts pure values to the monadic wrapper and back from the monadic wrapper to the pure value.
  • A combinator (also called a flat map) is a method that accepts a monadic type and a transformation function and returns another monadic type. It is how any proper operation on the input gets done. The combinator takes the transformation function, which unwraps the initial monadic type and performs an operation on the pure value before wrapping it again in a new monadic type.
A depiction of how monads work in a computer program

Monad Examples

Whether they realize it or not, many programmers have already used monads before in types like optional (called maybe monad) and nullables, which account for the possibility of missing values, or promise, which account for the possibility of values becoming available at a later point in time. The transformation chart above can serve as an accurate guide, no matter what type of monad is being used.

Either

Either is a powerful monadic type because of its ability to handle any type of error. It’s templated in nature, with a right template type for the happy path return type and a left template type for errors from the unhappy path.

In this section, we will implement a simplistic Either to show how monads work. There are two ways to utilize monadic patterns: chaining them and then using comprehensions. By comparing the two, we are able to return to writing imperative code. Writing code that uses monadic types is contagious and results in more explicit code that’s harder to get wrong because of the compiler guarantees.

As discussed earlier, a monad has three components: type constructor, type convertor, and combinator. Let’s go through the process of creating each one.

Type Constructor

This class represents monadic representation, which holds the pure value.

sealed class Either<out T, out U> {
   class Left<T>(val value: T) : Either<T, Nothing>()
   class Right<U>(val value: U) : Either<Nothing, U>()
}

Type Converter

These are type converters from pure to monadic classes.

fun <T> T.left (): Either<T, Nothing> = Left(this)
fun <U> U.right (): Either<Nothing, U> = Right(this)

and from monadic to pure values…

fun <T, U> Either<T, U>.bind(): U {
   return when (this) {
       is Right -> this.value
       is Left -> {
           // For a real-life example, maybe log the exception in the left and return an internal server error
           println("Caught an exception ${this.value}")
           exitProcess(1)
       }
   }
}

Combinator

This combinator changes the right from U to V, using the transformRight method. Note that this method only executes transformRight on the right value. If Either turns out to be a left value, then it doesn’t do anything.

fun <T, U, V> Either<T, U>.flatMap(transformRight: (U) -> Either<T, V>): Either<T, V> {
   return when (this) {
       is Right -> transformRight(this.value)
       is Left -> this
   }
}

Building Blocks

class Error(override val message: String?): Exception()

private fun returnFalse80PercentOfTheTime() = Random.nextInt(0, 100) % 5 == 0

fun throwRandomly(input: String): Either<Error, String> {
   if (returnFalse80PercentOfTheTime()) {
       return Error("Didn't Survive: $input").left()
   }

   return "Survived $input!".right()
}

Monad Chaining

This is the programming paradigm of method chaining.

fun chaining(): Any {
   val input = "Chaining"
   return throwRandomly(input)
       .flatMap(::throwRandomly)
       .bind()
}

Monad Comprehension

Chaining methods is in some ways undesirable because one has to write code in many different scopes. Wrapping and unwrapping of the monadic types can be used to produce the well-known imperative code once again.

fun comprehension(): Any {
   val input = "Comprehension"
   val intermediate = throwRandomly(input).bind()
   return throwRandomly(intermediate).bind()
}

Exceptions to Monadic Type

Sometimes, however, we deal with external code and libraries that tend to throw exceptions. To integrate these code fringes, we can use the catchEither method to transform the throwable into a left value.

fun throwReally() {
   throw Error("Error generated in throwReally.")
}

fun <R> catchEither(f: () -> R): Either<Throwable, R> =
   try { f().right() } catch (t: Throwable) { t.left() }

Driver Program

fun main() {
   val result = chaining()
   println(result)

   val easier = comprehension()
   println(easier)

   monadicExceptionDemo()
}

Possible Outcomes

Caught an exception Error: Didn't Survive: Chaining

or

Survived Survived Chaining!!
Caught an exception Error: Didn't Survive: Comprehension

or

Caught an exception Error: Didn't Survive: Survived Chaining!

or

Survived Survived Chaining!!
Survived Survived Comprehension!!
Caught an exception Error: Error generated in throwReally.

Conclusion

As horizontal scaling has come to dominate modern computing, different machines have had to collaborate over the network interface. I/O operations like waiting for a user’s input or networking are long blocking calls that hang the system if they are not managed judiciously. Programmers have developed a mechanism called continuation-passing to handle this problem.

Continuation-passing has come with many uncertainties that complicate the codebase and clutter programmers’ reasoning. Monads, from category theory, were used to simplify code and make it less error-prone, which has increased developer productivity and reduced the number of bugs produced.

Now that you understand monads, how will you leverage their use in your code base?

Acknowledgment

This article has drawn substantially upon information from Arrow in Kotlin, a useful web platform that the author recommends to interested programmers.

Appendix

  • Node Js uses libuv’s event loop

Appendix: Formal Explanations of the Monads

  • Given any well-defined, basic types T, U, a monad consists of three parts:
  • A type constructor M that builds up a monadic type M T
  • A type converter often called unit or return: unit: T → M T
  • A combinator, typically called bind and represented with an infix operator >>= or a method called flatMap, unwraps a monadic variable, then inserts it into a monadic function/expression, resulting in a new monadic value: (>>=) : (M T, T → M U) → M U
  • Putting it all together: if mx: M T and f : T → M U, then (mx >>= f): M U