header source
my icon
esplo.net
ぷるぷるした直方体
Cover Image for Scala Functional Design & Programming - Chapter 4 (4)

Scala Functional Design & Programming - Chapter 4 (4)

about10mins to read

I bought "Scala Functional Design & Programming - A Comprehensive Guide to Functional Programming with Scalaz".
I'll read it little by little and summarize each chapter.

amazon img

This time, I've reached Chapter 4. It's about practical error handling.

https://www.esplo.net/posts/2016/09/sfdp-3

Chapter 4

This chapter discusses error handling.
First, it explains the problems with exceptions, such as lack of referential transparency, type safety, and poor compatibility with high-order functions like map.
Instead, it presents methods that return null or special values, or pass error handling functions as arguments, and lists their problems.

Option

As an alternative approach, the Option type is introduced.
Here, we're given an exercise to implement map, flatMap, getOrElse, orElse, and filter for Option.

  def map[B](f: A => B): Option[B] =
    this match {
      case None => None
      case Some(a) => Some(f(a))
    }

  def getOrElse[B>:A](default: => B): B =
    this match {
      case None => default
      case Some(a) => a
    }

  def flatMap[B](f: A => Option[B]): Option[B] =
    this.map(f).getOrElse(None)

  def orElse[B>:A](ob: => Option[B]): Option[B] =
    this.map(Some(_)).getOrElse(ob)

  def filter(f: A => Boolean): Option[A] =
    this.flatMap(a => if(f(a)) Some(a) else None)

Although Option can be a bit complicated when nested, we can write necessary processes and types smoothly.

Implement the variance function using flatMap.

We can write it without worrying about errors.

  def variance(xs: Seq[Double]): Option[Double] =
    mean(xs).flatMap(m => mean(xs.map(a => math.pow(a - m, 2))))

The convenient and safe Option type makes us wonder if we need to rewrite all previous functions to support it.
To address this concern, we use the concept of lift to make ordinary functions Option-compatible.

Implement map2, which takes two Option values and a two-argument function.

  def map2[A,B,C](a: Option[A], b: Option[B])(f: (A, B) => C): Option[C] =
    a.flatMap(sa => b.map(sb => f(sa, sb)))

Implement the sequence function, which returns None if any of the input Options are None, and returns a list of all Some values otherwise.

We can use foldRight and map2 to implement it.

  def sequence[A](a: List[Option[A]]): Option[List[A]] =
    a.foldRight(Some(Nil): Option[List[A]]) ((x, y) => map2(x, y)(_ :: _))

Implement the traverse function, which is a generalized version of sequence.

It's almost the same as sequence.

  def traverse[A, B](a: List[A])(f: A => Option[B]): Option[List[B]] =
    a.foldRight(Some(Nil): Option[List[B]]) ((x, y) => map2(f(x), y)(_ :: _))

Either

While Option informs us of errors, it doesn't provide additional information.
To address this, we use the Either data type to represent errors with additional information.
Either is a data type that consists of the direct sum of Right (normal value) and Left (error value).

Here's another exercise.

Implement map, flatMap, orElse, and map2 for Right values.

  def map[B](f: A => B): Either[E, B] =
    this match {
      case Right(v) => Right(f(v))
      case Left(v) => Left(v)
    }

  def flatMap[EE >: E, B](f: A => Either[EE, B]): Either[EE, B] =
    this match {
      case Right(v) => f(v)
      case Left(v) => Left(v)
    }

  def orElse[EE >: E, B >: A](b: => Either[EE, B]): Either[EE, B] =
    this match {
      case Right(v) => Right(v)
      case Left(_) => b
    }

  def map2[EE >: E, B, C](b: Either[EE, B])(f: (A, B) => C): Either[EE, C] =
    flatMap(a => b.map(f(a, _)))

The combination of flatMap and map can be written using for syntax, which is a syntax sugar.
Although I'm not used to it, I'd like to get accustomed to using for.

Implement sequence and traverse.

  def sequence[E,A](es: List[Either[E,A]]): Either[E,List[A]] =
    es.foldRight(Right(Nil): Either[E, List[A]])((a, b) => a.map2(b)(_ :: _))

  def traverse[E,A,B](es: List[A])(f: A => Either[E, B]): Either[E, List[B]] =
    es.foldRight(Right(Nil): Either[E, List[B]])((a, b) => f(a).map2(b)(_ :: _))

We can also implement sequence using traverse.
If we make the A in traverse an Either[E, B], it's done.

What should we change to report multiple errors when using map2?

Since Either can only hold one error, we can modify it to hold a list of errors.
The idea is to add errors to the list as they occur.

That's the end of Chapter 4.
After the intense exercises in Chapter 3, I felt a bit unsatisfied with the length of this chapter.
However, each chapter should be around this length, and there's still a long way to go.

Next, we'll discuss non-strictness.

Thoughts

This chapter dealt with error handling, which I think is particularly useful in actual programming.
While exceptions can be handled quickly, they have significant drawbacks, such as disrupting the flow and compromising type safety.
I felt that this chapter was packed with techniques to preserve purity by treating errors as values.

Continued:
https://www.esplo.net/posts/2016/10/sfdp-5

Share