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] =
.fromOption[Try](option_sqrt(a)) flatMap (b => OptionT.liftF(try_sqrt(b)) OptionT
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.
Citation
@online{law2017,
author = {Jonny Law},
title = {Using {Monads} for {Handling} {Failures} and {Exceptions}},
date = {2017-01-04},
langid = {en}
}