slide /

Comprehending Monads

6th Conference on Lisp and Functional Programming, 1992

Example 1: Coping with Failure

... in Java, early-return style :(

public class Foo {
	public Bar getBar();
}

public class Bar {
	public Baz getBaz();
}

public class Baz {
	public int compute();
}
public Integer compute(Foo foo) {
	if (foo == null)
		return null;
	Bar bar = foo.getBar();
	if (bar == null)
		return null;
	Baz baz = bar.getBaz();
	if (baz == null)
		return null;
	return baz.compute();
}

Example 1: Coping with Failure

... in Java, nested-if style :(

public class Foo {
	public Bar getBar();
}

public class Bar {
	public Baz getBaz();
}

public class Baz {
	public int compute();
}





public Integer compute(Foo foo) {
	if (foo != null) {
		Bar bar = foo.getBar();
		if (bar != null) {
			Baz baz = bar.getBaz();
			if (baz != null)
				return baz.compute();
			else
				return null;
		}
		else
			return null;
	}
	else
		return null;
}

Example 1: Coping with Failure

... in Scala, Java-style :(

class Foo {
	def bar: Bar
}

class Bar {
	def baz: Baz
}

class Baz {
	def compute: Int
}




def compute(foo: Foo): Int =
	if (foo != null) {
		val bar = foo.bar
		if (bar != null) {
			val baz = bar.baz
			if (baz != null)
				baz.compute
			else
				null
		}
		else
			null
	}
	else
		null

What's a monad?

What's a monad?

trait Monad[A] {
	def map[B](f: A => B): Monad[B]
	def flatMap[B](f: A => Monad[B]): Monad[B]
}

Note: there is no single, special Monad base trait!

Provides a standard interface for composing and sequencing operations on some contained value(s)

The Option monad

The Option monad

sealed trait Option[A]

case class Some[A](a: A) extends Option[A]
case class None[A] extends Option[A]

The Option monad

sealed trait Option[A] {
	def map[B](f: A => B): Option[B]
	def flatMap[B](f: A => Option[B]): Option[B]
}

case class Some[A](a: A) extends Option[A]
case class None[A] extends Option[A]

The Option monad

sealed trait Option[A] {
	def map[B](f: A => B): Option[B]
	def flatMap[B](f: A => Option[B]): Option[B]
}

case class Some[A](a: A) extends Option[A] {
	def map[B](f: A => B): Option[B] = new Some(f(a))
	def flatMap[B](f: A => Option[B]): Option[B] = f(a)
}

case class None[A] extends Option[A]

The Option monad

sealed trait Option[A] {
	def map[B](f: A => B): Option[B]
	def flatMap[B](f: A => Option[B]): Option[B]
}

case class Some[A](a: A) extends Option[A] {
	def map[B](f: A => B): Option[B] = new Some(f(a))
	def flatMap[B](f: A => Option[B]): Option[B] = f(a)
}

case class None[A] extends Option[A] {
	def map[B](f: A => B): Option[B] = new None
	def flatMap[B](f: A => Option[B]): Option[B] = new None
}

Note: this differs from the standard scala.Option

Example 1: Coping with Failure

... in Scala, using the Option monad

class Foo { def bar: Option[Bar] }
class Bar { def baz: Option[Baz] }
class Baz { def compute: Int }

def compute(maybeFoo: Option[Foo]): Option[Int] = // ???

Example 1: Coping with Failure

... in Scala, using the Option monad

class Foo { def bar: Option[Bar] }
class Bar { def baz: Option[Baz] }
class Baz { def compute: Int }

def computeBaz(baz: Baz) = // ???
def computeBar(bar: Bar) = // ???
def computeFoo(foo: Foo) = // ???
def compute(maybeFoo: Option[Foo]): Option[Int] = // ???

Example 1: Coping with Failure

... in Scala, using the Option monad

class Foo { def bar: Option[Bar] }
class Bar { def baz: Option[Baz] }
class Baz { def compute: Int }

def computeBaz(baz: Baz): Int =
	baz.compute

def computeBar(bar: Bar) = // ???
def computeFoo(foo: Foo) = // ???
def compute(maybeFoo: Option[Foo]): Option[Int] = // ???

Example 1: Coping with Failure

... in Scala, using the Option monad

class Foo { def bar: Option[Bar] }
class Bar { def baz: Option[Baz] }
class Baz { def compute: Int }

def computeBaz(baz: Baz): Int =
	baz.compute

def computeBar(bar: Bar): Option[Int] =
	bar.baz.map { computeBaz }

def computeFoo(foo: Foo) = // ???
def compute(maybeFoo: Option[Foo]): Option[Int] = // ???

Example 1: Coping with Failure

... in Scala, using the Option monad

class Foo { def bar: Option[Bar] }
class Bar { def baz: Option[Baz] }
class Baz { def compute: Int }

def computeBaz(baz: Baz): Int =
	baz.compute

def computeBar(bar: Bar): Option[Int] =
	bar.baz.map { computeBaz }

def computeFoo(foo: Foo): Option[Int] =
	foo.bar.flatMap { computeBar }

def compute(maybeFoo: Option[Foo]): Option[Int] = // ???

Example 1: Coping with Failure

... in Scala, using the Option monad

class Foo { def bar: Option[Bar] }
class Bar { def baz: Option[Baz] }
class Baz { def compute: Int }

def computeBaz(baz: Baz): Int =
	baz.compute

def computeBar(bar: Bar): Option[Int] =
	bar.baz.map { computeBaz }

def computeFoo(foo: Foo): Option[Int] =
	foo.bar.flatMap { computeBar }

def compute(maybeFoo: Option[Foo]): Option[Int] =
	maybeFoo.flatMap { computeFoo }

Example 1: Coping with Failure

... in Scala, using the Option monad

class Foo { def bar: Option[Bar] }
class Bar { def baz: Option[Baz] }
class Baz { def compute: Int }

def compute(maybeFoo: Option[Foo]): Option[Int] =
	maybeFoo.flatMap { foo =>
		foo.bar.flatMap { bar =>
			bar.baz.map { baz =>
				baz.compute
			}
		}
	}

Better, but hard to read. We'll come back to it...

Example 2: Coping with Informative Failure

... in Java, the easy way

public class Foo { public Bar getBar() throws Exception; }
public class Bar { public Baz getBaz() throws Exception; }
public class Baz { public int compute() throws Exception; }

public Integer compute(Foo foo) throws Exception {
	return foo.getBar().getBaz().compute();
}

Very easy to read, but sloppy engineering style!

Example 2: Coping with Informative Failure

... in Java, the hard way

public class Foo { public Bar getBar() throws GetBarException; }
public class Bar { public Baz getBaz() throws GetBazException; }
public class Baz { public int compute() throws ComputeException; }

public Integer compute(Foo foo) throws ComputeException {
	try {
		foo.getBar().getBaz().compute();
	}
	catch (GetBarException e) {
		throw new ComputeException(e);
	}
	catch (GetBazException e) {
		throw new ComputeException(e);
	}
}

Much clumsier :(

The Validation monad

The Validation monad

sealed trait Validation[E, A] {
	def map[B](f: A => B): Validation[E, B]
	def flatMap[B](f: A => Validation[E, B]): Validation[E, B]
	def liftFail[F](f: E => F): Validation[F, A] // unrelated to monads!
}

case class Success[E, A](a: A) extends Validation[E, A] {
	def map[B](f: A => B): Validation[E, B] = new Success(f(a))
	def flatMap[B](f: A => Validation[E, B]): Validation[E, B] = f(a)
	def liftFail[F](f: E => F): Validation[F, A] = new Success(a)
}

case class Failure[E, A](e: E) extends Validation[E, A] {
	def map[B](f: A => B): Validation[E, B] = new Failure(e)
	def flatMap[B](f: A => Validation[E, B]): Validation[E, B] = new Failure(e)
	def liftFail[F](f: E => F): Validation[F, A] = new Failure(f(e))
}

Note: based loosely on Validation from the scalaz library (https://github.com/scalaz/scalaz).

Example 2: Coping with Informative Failure

... in Scala, using the Validation monad

class Foo { def bar: Validation[BarException, Bar] }
class Bar { def baz: Validation[BazException, Baz] }
class Baz { def compute: Validation[ComputeException, Int] }

def compute(foo: Foo): Validation[ComputeException, Int] =
	foo.bar.liftFail { new ComputeException(_) }.flatMap { bar =>
		bar.baz.liftFail { new ComputeException(_) }.flatMap { baz =>
			baz.compute
		}
	}

Again: better, but hard to read.

Also inconsistent with the pattern we saw in Option.

Example 3: Coping with Nondeterminism

... in Java

public class Foo {
	public List<Bar> getBars();
}

public class Bar {
	public List<Baz> getBazs();
}

public class Baz {
	public List<Integer> computeAll();
}
public List<Integer> computeAll(List<Foo> foos) {
	List<Integer> results = new ArrayList<Integer>();
	for (Foo foo: foos) {
		for (Bar bar: foo.getBars()) {
			for (Baz baz: bar.getBazs()) {
				results.addAll(baz.computeAll());
			}
		}
	}
	return results;
}

Actually not too bad, if you ignore the mutable ArrayList...

Example 3: Coping with Nondeterminism

... in Scala

class Foo { def bars: List[Bar] }
class Bar { def bazs: List[Baz] }
class Baz { def computeAll: List[Int] }

def computeAll(foos: List[Foo]): List[Int] =
	for {
		foo <- foos
		bar <- foo.bars
		baz <- bar.bazs
		results <- baz.computeAll
	} yield results

Very readable! But what does this have to do with monads?

Surprise!

Formal definition of for-comprehensions

for { p0 <- e0 } yield e
e0.map { p0 => e }
for {
	p0 <- e0
	p1 <- e1
	...
	pn <- en
} yield e

e0.flatMap { p0 =>
	for {
		p1 <- e1
		...
		pn <- en
	} yield e
}

... so for-comprehensions operate on any monad.

Example 3 Revisited: map and flatMap

def computeAll(foos: List[Foo]): List[Int] =
	for {
		foo <- foos
		bar <- foo.bars
		baz <- bar.bazs
		results <- baz.computeAll
	} yield results

Example 3 Revisited: map and flatMap

def computeAll(foos: List[Foo]): List[Int] =
	foos.flatMap { foo =>
		for {
			bar <- foo.bars
			baz <- bar.bazs
			results <- baz.computeAll
		} yield results
	}

Example 3 Revisited: map and flatMap

def computeAll(foos: List[Foo]): List[Int] =
	foos.flatMap { foo =>
		for {
			bar <- foo.bars
			baz <- bar.bazs
			results <- baz.computeAll
		} yield results
	}

Example 3 Revisited: map and flatMap

def computeAll(foos: List[Foo]): List[Int] =
	foos.flatMap { foo =>
		foo.bars.flatMap { bar =>
			for {
				baz <- bar.bazs
				results <- baz.computeAll
			} yield results
		}
	}

Example 3 Revisited: map and flatMap

def computeAll(foos: List[Foo]): List[Int] =
	foos.flatMap { foo =>
		foo.bars.flatMap { bar =>
			for {
				baz <- bar.bazs
				results <- baz.computeAll
			} yield results
		}
	}

Example 3 Revisited: map and flatMap

def computeAll(foos: List[Foo]): List[Int] =
	foos.flatMap { foo =>
		foo.bars.flatMap { bar =>
			bar.bazs.flatMap { baz =>
				for { results <- baz.computeAll } yield results
			}
		}
	}

Example 3 Revisited: map and flatMap

def computeAll(foos: List[Foo]): List[Int] =
	foos.flatMap { foo =>
		foo.bars.flatMap { bar =>
			bar.bazs.flatMap { baz =>
				for { results <- baz.computeAll } yield results
			}
		}
	}

Example 3 Revisited: map and flatMap

def computeAll(foos: List[Foo]): List[Int] =
	foos.flatMap { foo =>
		foo.bars.flatMap { bar =>
			bar.bazs.flatMap { baz =>
				baz.computeAll.map { results => results }
			}
		}
	}

Example 3 Revisited: map and flatMap

def computeAll(foos: List[Foo]): List[Int] =
	foos.flatMap { foo =>
		foo.bars.flatMap { bar =>
			bar.bazs.flatMap { baz =>
				baz.computeAll
			}
		}
	}

This pattern looks familiar...

Let's beautify the first two examples.

Example 1 Revisited: for-comprehensions

def compute(maybeFoo: Option[Foo]): Option[Int] =
	maybeFoo.flatMap { foo =>
		foo.bar.flatMap { bar =>
			bar.baz.map { baz => baz.compute }
		}
	}

Example 1 Revisited: for-comprehensions

def compute(maybeFoo: Option[Foo]): Option[Int] =
	maybeFoo.flatMap { foo =>
		foo.bar.flatMap { bar =>
			for { baz <- bar.baz } yield baz.compute
		}
	}

Example 1 Revisited: for-comprehensions

def compute(maybeFoo: Option[Foo]): Option[Int] =
	maybeFoo.flatMap { foo =>
		foo.bar.flatMap { bar =>
			for { baz <- bar.baz } yield baz.compute
		}
	}

Example 1 Revisited: for-comprehensions

def compute(maybeFoo: Option[Foo]): Option[Int] =
	maybeFoo.flatMap { foo =>
		for {
			bar <- foo.bar
			baz <- bar.baz
		} yield baz.compute
	}

Example 1 Revisited: for-comprehensions

def compute(maybeFoo: Option[Foo]): Option[Int] =
	maybeFoo.flatMap { foo =>
		for {
			bar <- foo.bar
			baz <- bar.baz
		} yield baz.compute
	}

Example 1 Revisited: for-comprehensions

def compute(maybeFoo: Option[Foo]): Option[Int] =
	for {
		foo <- maybeFoo
		bar <- foo.bar
		baz <- bar.baz
	} yield baz.compute

The monadic weight loss program

Before (Java)

if (foo != null) {
	Bar bar = foo.getBar();
	if (bar != null) {
		Baz baz = bar.getBaz();
		if (baz != null)
			return baz.compute();
		else
			return null;
	}
	else
		return null;
}
else
	return null;

Conflation of concerns: "coping with failure" is intertwined with the computation.

After (Scala)

for {
	foo <- maybeFoo
	bar <- foo.bar
	baz <- bar.baz
} yield baz.compute

Separation of concerns: the Option monad encapsulates the "coping mechanism."

Example 2 Revisited: for-comprehensions

def compute(foo: Foo): Validation[ComputeException, Int] =
	foo.bar.liftFail { new ComputeException(_) }.flatMap { bar =>
		bar.baz.liftFail { new ComputeException(_) }.flatMap { baz =>
			baz.compute
		}
	}

Example 2 Revisited: for-comprehensions

def compute(foo: Foo): Validation[ComputeException, Int] =
	foo.bar.liftFail { new ComputeException(_) }.flatMap { bar =>
		bar.baz.liftFail { new ComputeException(_) }.flatMap { baz =>
			baz.compute.map { result => result }
		}
	}

Example 2 Revisited: for-comprehensions

def compute(foo: Foo): Validation[ComputeException, Int] =
	for {
		bar <- foo.bar.liftFail { new ComputeException(_) }
		baz <- bar.baz.liftFail { new ComputeException(_) }
		result <- baz.compute
	} yield result

Looks strikingly similar to the Option example

def compute(maybeFoo: Option[Foo]): Option[Int] =
	for {
		foo <- maybeFoo
		bar <- foo.bar
		baz <- bar.baz
	} yield baz.compute

Monad Roundup

Monad Roundup

Monads capture policies for function application and composition, in map and flatMap

for-comprehensions provide a sequential syntax, on top of map and flatMap

... and more.