Sunday, April 26, 2020

Handling Scala 2.x Option Elegantly

Target audience: Beginner
Estimated reading time: 3'

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

Table of contents
Follow me on LinkedIn

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++.

Notes:
  • 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 
  • The code associated with this article is written using Scala 2.12.4

Use case

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

Clearly, there is no need to compute y if x is negative.
The most common to handle a Scala option is to unwrap it. Let's consider the function      y = sin(sqrt(x))
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.

Thank you for reading this article. For more information ...

References


---------------------------
Patrick Nicolas has over 25 years of experience in software and data engineering, architecture design and end-to-end deployment and support with extensive knowledge in machine learning. 
He has been director of data engineering at Aideo Technologies since 2017 and he is the author of "Scala for Machine Learning" Packt Publishing ISBN 978-1-78712-238-3