Skip to content

Instantly share code, notes, and snippets.

@KatrinaHoffert
Last active August 29, 2015 14:14
Show Gist options
  • Save KatrinaHoffert/b774f042213e26a1b4bb to your computer and use it in GitHub Desktop.
Save KatrinaHoffert/b774f042213e26a1b4bb to your computer and use it in GitHub Desktop.
Scala version of this SO question: http://stackoverflow.com/a/194207/1968462 (also drawing from http://stackoverflow.com/a/28139260/1968462)

First: The term monad is a bit vacuous if you are not a mathematician. An alternative term is computation builder which is a bit more descriptive of what they are actually useful for.

You ask for practical examples:

Example 1: Handling would-be partial functions:

def divide(numerator: Int, denominator: Int): Option[Int] = {
	if(denominator != 0) Some(numerator / denominator)
	else None
}

This code would perform division while performing division when it's possible while still returning something when we cannot perform (integer) division, which is when the denominator is zero.

Now suppose we also have a simple double function:

def double(x: Int): Int = x * 2

And we wanted to nest these: double(divide(x, y)). Problem is that we can't nest these because their types are not compatible. One returns an Option[Int] and the other expects an Int. But the boxed type (Int) is certainly compatible.

The map[T, U](f: T => U) function allows us to handle this. For Option, the type signature would be map[T, U](f: T => U): Option[U]. That is, it will map our Option[T] into an Option[U] based on the function passed in. More specifically, if the boxing type is Some, then we will call the passed in function, f, applying it to the boxed in value. But if the boxing type is None, we will simply return a None[U] (and thus the function is not called).

So the way to nest these would be with divide(x, y) map double.

Example 2: For comprehensions:

for {
    divisionResult <- divide(x, y)
    doubled <- Option(double(divisionResult))
    halved <- divide(doubled, 2)
} yield halved

This is, in fact, syntax sugar for:

divide(x, y) flatMap { divisionResult =>
    Option(double(divisionResult)) flatMap { doubled =>
        divide(doubled, 2)
    }
}

Flatmap works like map, but is for the case of two boxed types of the same kind, where we only want one type. If we used map here, we'd end up with Option[Option[Option[Int]]]. Flatmap merely flattens this out (eg, Option[None[Int]] would become simply None[Int]). You'll note that the third line had to have the result wrapped into an Option, since for comprehensions depend on there being the flatMap method. So Option.flatMap's type signature would be something like flatMap[T, U](f: T => Option[U]): Option[U].

The for comprehensions are very useful for use with the Try type, since we can chain together a bunch of commands that may throw an exception (why is boxed into the Try object). If that happens, all the other mappings in the chain (that is, all the composed functions) will be ignored (since flatMap for Try will only run the passed in function on a Success; if the Try is a Failure, it will not execute the passed in function -- that'll happen for all the functions in the chain).

How it works:

Both examples uses monads, aka computation builders. The common theme is that the monad chains operations in some specific, useful way. In both cases, we used the Option monad to box up our types and then used map and flatMap to chain the operations together. So for Scala, monads are simply objects with the map and flatMap methods. For example, here's my own quick implementation of Option (not a very good implementation as it tries to keep things simple and small):

abstract class MyOption[T] {
    def map[U](f: T => U): MyOption[U]
    def flatMap[U](f: T => MyOption[U]): MyOption[U]
}

object MyOption {
    def apply[T](value: T): MyOption[T] = MySome[T](value)
}

case class MySome[T](value: T) extends MyOption[T] {
    override def map[U](f: T => U): MyOption[U] = MySome[U](f(value))
    def flatMap[U](f: T => MyOption[U]): MyOption[U] = {
        f(value) match {
            case MySome(nestedOption) => MySome(nestedOption)
            case MyNone() => MyNone[U]
        }
    }
}

case class MyNone[T] extends MyOption[T] {
    override def map[U](f: T => U): MyOption[U] = MyNone[U]
    def flatMap[U](f: T => MyOption[U]): MyOption[U] = MyNone[U]
}

It turns out the pattern of chaining operations is quite useful. Another example is exceptions: Using the Try monad, operations are chained such that they are performed sequentially, except if an error is thrown, in which case the rest of the chain is abandoned.

So I can now use my custom type in a for comprehension:

def divide(numerator: Int, denominator: Int): MyOption[Int] = {
    if(denominator != 0) MySome(numerator / denominator)
    else MyNone[Int]
}

def double(x: Int): Int = x * 2

def divideDoubleHalf(x: Int, y: Int): MyOption[Int] = {
    for {
        divisionResult <- divide(x, y)
        doubled <- MyOption(double(divisionResult))
        halved <- divide(doubled, 2)
    } yield halved
}

divideDoubleHalf(5, 2) // res21: MyOption[Int] = MySome(2)
divideDoubleHalf(5, 0) // res22: MyOption[Int] = MyNone()

Summing up

In Scala-terms a monad is a parameterized type which provides a map and flatMap function which have type signatures similar to. We could simply consider the types that a for comprehension can parse to be monads:

def map[U](f: T => U): MonadType[U]
def flatMap[U](f: T => MonadType[U]): MonadType[U]

In itself map and flatMap is just a cumbersome way of chaining functions, but with the presence of for comprehensions, which hides the "plumbing", the monadic operations turns out to be a very nice and useful abstraction.

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