Monday, June 23, 2014

Handling Scala Option Elegantly

This post reviews the different alternative mechanisms in Scala to handle errors. It also illustrates the applicability of the Option monad.

Overview
The Option monad is a great tool for handling error, in Scala: developers do not have worry about NullPointerException or handling a typed exception as in Java and C++.

Note: For the sake of readability of the implementation of algorithms, all non-essential code such as error checking, comments, exception, validation of class and method arguments, scoping qualifiers or import is omitted

Let's consider the simple square root computation, which throws an exception if the input value is strictly negative (line 2). The most common "java-like" approach is to wrap the computation with a try - catch paradigm. In Scala catching exception can be implemented through the Try monad (lines 7-9).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def sqrt(x: Double): Double = 
  if(x < 0.0) 
    throw MathException(s"sqrt: Incorrect argument $x")
  else 
    Math.sqrt(x)
 
Try ( sqrt(a)) match {
  case Success(x) => {}
  case Failure(e) => Console.println(e.toString)
}

This type of implementation put the burden on the client code to handle the exception. The Option monad provides developer an elegant to control the computation flow.

Handling Option values
The most common to handle a Scala option is to unwrap it. Let's consider the function
 
  y = sin(sqrt(x))
Clearly, there is no need to compute y if x is negative.

def sqrt(x: Double): Option[Double] = {
  if(x < 0.0) None
  else Math.sqrt(x)
}
 
def y(x: Double): Option[Double] = sqrt(x) match {
  case Some(y) => Math.sin(x)
  case None => None
}

The computation of the square root is implemented by the method sqrt while the final computation of sin(sqrt(x)) is defined by the method y.

This implementation is quite cumbersome because the client code has to process an extra Option. An alternative is to provide a default value (i.e 0.0) if the first computational step fails.

def y(x: Double): Double = Math.sin(sqrt(x)).getOrElse(0.0)

A more functional and elegant approach uses the map higher order function to propagate the value of the Option.

def y(x: Double): Double = 
   sqrt(x).map(Math.sin(_)).getOrElse(0.0)

What about a sequence of nested options? Let's consider the function y = 1/sqrt(x). There are two types of errors:
  • x < 0.0 for sqrt
  • x == 0.0 for 1/x
A third solution consist of applying the test for x > 0.0 to meet the two conditions at once.

def y(xdef y(x: Double): Double = 
  if(x < 1e-30) None
  else Some(1.0/(Math.sqrt(x)))

for comprehension for options
However anticipating the multiple complex conditions on the argument is not always possible. The for comprehensive for loop is an elegant approach to handle sequence of options.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def inv(x: Double): Option[Double] = {
  if(Math.abs(x) < 1e-30) None
  else 1.0/x
}

def log(x: Double): Option[Double] = {
  if(x < 1e-30) None
  else Math.log(x)
}
 
def compose(x: Double): Double =
 (for {
    y <- sqrt(x)
    z <- inv(y)
    t <- log(z)
  } yield t).getOrElse(0.0)

The objective is to compose the computation of a square root with the inverse function inv (line 1) and natural logarithm log (line 6). The for comprehension construct (lines 11-15) propagates the result of each function to the next in the pipeline through the automatic conversion of option to its value. In case of error (None), the for method exists before completion.
For-comprehension is a monad that compose (cascading) multiple flatMap with a final map method.
  for {
    a <- f(x)   // flatMap
    b <- g(a)   // flatMap
    c <- h(b)   // map
  } yield c 

References

No comments:

Post a Comment