Using Monads for Handling Failures and Exceptions

In this post I will give a practical introduction to some useful structures for handling failure in functional programming.

Referential Transparency

One of the most important properties of functional programming is referential transparency and programming with pure functions. This means we can substitute a pure function with its result, for intance if we have the function def f = 1 + 2, we can replace every occurence of f with 3 and the final evaluation will remain unchanged

This simple idea can lead to difficulties when considering functions which involve side effects, such as reading from external sources or generating random numbers. One example of a side effect is an exception, an imperative programmer might write a function to calculate a square root as:

def unsafe_sqrt(a: Double): Double = {
  if (a > 0) math.sqrt(a)
  else throw new Exception("Can't calculate square root of negative number")
}

This compiles fine, however if we wrote this function for an end user and they didn’t look at the implementation they might not know the function can possibly return an exception.

Try

In order to make it clear that a function can fail, we can return a Try:

def try_sqrt(a: Double): Try[Double] = {
  if (a > 0) Success(math.sqrt(a))
  else Failure(throw new Exception("Can't calculate square root of negative number"))
}

Now, if someone were to use this function they would be forced to deal with the Try return type and understand that the function can return an exception. Try is actually an algebraic datatype (ADT), an illustrative implementation is:

sealed trait Try[+A]
case class Success[+A](a: A) extends Try[A]
case class Failure[+A](exception: Throwable) extends Try[A]

This means that a Try can either be a Success or Failure. Learn more about Try in Daniel Westheide’s excellent Neophyte’s Guide to Scala.

Option

Another simple structure to represent computations which may fail is Option, this is an algebraic datatype:

sealed trait Option[+A]
case class Some[+A](a: A) extends Option[A]
case class None extends Option[Nothing]

In this case, option can either contain a value using the constructor Some, or can represent the absense of a value using None. This provides less information on failure that Try, but nevertheless is sometimes useful. We can re-write the sqrt function to return an optional value

def option_sqrt(a: Double): Option[Double] = {
  if (a > 0) Some(math.sqrt(a))
  else None
}

Now, when provide an incorrect argument to the function, we get None as the result.

Chaining Computations

In real world functional codebases we compose programs from many small functions. Let’s consider the problem of how to apply def sqrt(a: Double): Option[Double] twice. A naive attempt would be to simply compose the functions as we would for the Scala math library:

def sqrt_twice = unsafe_sqrt _ compose unsafe_sqrt _

The _ represents partial application of unsafe_sqrt, if we try to compose the option_sqrt function as in this example we will get a type mismatch. One application of option_sqrt returns a typle Option[Double], but we need the type Double. Luckily Option has a function defined on it for composing operations like this:

def flatMap[A, B](a: Option[A])(f: A => Option[B]): Option[B]

We can now use flatMap to compose option_sqrt:

def sqrt_twice_option(x: Double): Option[Double] = 
  option_sqrt(x) flatMap option_sqrt

Now we can calculate sqrt_twice_option(81) = Some(3.0).

We can compose try_sqrt in the same way:

def sqrt_twice_try(x: Double): Try[Double] = 
  try_sqrt(81) flatMap try_sqrt

Now, what if we want to compose option_sqrt and try_sqrt. This is not an easy problem in general, however the Scala standard library implements a toOption method on Try values. Hence we can just convert the output of try_sqrt to an Option, however we lose the text from the exception upon failure, which could illuminating in the event of a failure. Let’s consider a more general way to compose the two.

Nested Maps

Option and Try are both monads (strictly Try is not a proper monad), which means they are equipped with two methods which satisfy the monad laws. The two methods defined for all monads are:

trait Monad[A, M[_]] {
  def flatMap[B](f: A => M[B]): M[B]
  def pure(a: A): M[A]
}

We can define all the functions on Try and Option using these two functions, for instance map:

def map[B](f: A => B): M[B] = this.flatMap(a => pure(f(a)))

Now, we can use the map function to compose option_sqrt and try_sqrt:

def sqrt_twice(a: Double): Try[Option[Double]] = try_sqrt(a) map option_sqrt

However, what if we want to apply another function to a value returned by this function:

def f(a: Double) = a + 1
sqrt_twice(81) map (_.map(f))
// Success(Some(4.0))

We get the correct value, but we have to apply map twice, this seems cumbersome. There is a better way!

Monad Transformers

The functional programming library cats, short for category, has some built in types for dealing with nesting in a more elegent way. The type OptionT[F[_], A] can be used instead of F[Option[A]], our F[_] type in this case is Try[A]

import cats.implicits._
import cats.data.OptionT

def sqrt_twice_trans(a: Double): OptionT[Try, Double] = 
  OptionT.fromOption[Try](option_sqrt(a)) flatMap (b => OptionT.liftF(try_sqrt(b))

OptionT provides the function fromOption to transform the result of the option_sqrt function into the OptionT monad. The function liftF is used to lift any monad, in this case Try into the OptionT monad. This compiles and we if we now try to apply the function def f(a: Double) = a + 1 to the result of this function we only need a single call to map. This is because OptionT is also a monad:

sqrt_twice_trans(81) map f
// OptionT(Success(Some(4.0)))

This may seem like quite a lot of effort to remove a call to map, but removing unecessary duplication can help with readability of code, and enable bugs to be spotted earlier. The code has been assembled in a Github gist.