2/n - How do I ... in FP: selective recovery on error

2/n - How do I ... in FP: selective recovery on error

Problem setup:

Let's say we have this ADT to model some errors

sealed abstract class AppError extends Product with Serializable
object AppError{
    case class ParseError(msg: String, field: String) extends AppError
    case class GeneralError(msg: String) extends AppError
    case class MissingValueError(name: String) extends AppError
}

And some existing method:

def readString(cfg: Config, propName: String): Either[AppError, String] = ??? 

How to recover with a default value when the result is Left(MissingValueError)?

The first thing to consider when working in a statically typed language (like Kotlin or Scala), is to get the most out of types. What do we want in this case ? From an Either[AppError, String] retrieve a new Either[AppError, String]. This determines the signature of our function to Either[AppError, String] => Either[AppError, String]. If we want to provide a default value we can deduce the following signature:

def recoverMissing(e: Either[AppError, String], defaultValue: String): Either[AppError, String])

A simple approach to implementing this function to pattern matching on e and reacting accordingly:

def recoverMissing(e: Either[AppError, String], defaultValue: String): Either[AppError, String]) = 
  e match {
    case Right(_) => e
    case Left(err) => err match {
      case mv @ AppError.MissingValueError(_) => Right(defaultValue)
      case _ => e
    }
  }

Generalizing

If we take a closer look at our implementation, we can see that we could generalize it, not only to handle AppError on the left side or String on the right side. We can come up with this function:

def recover[E, A](e: Either[E, A])(f: E => Either[E,A]): Either[E, A] = 
  e match {
    case Right(_) => e
    case Left(err) => f(err)

This way, we let a chance to the function f to transform an error into a succes, regardless of any type (we have no constraint on type E and A.

For example we can run it this way:

val inError: Either[Int, String] = Left(42)
recover(inError){ codeErr =>
  if (codeErr == 42)
    Right("42 is valid!")
  else 
    inError
}

// Returns: 
// Right("42 is valid!")

Now, what if we are in a library which provide the default value feature. In this case, may be our users would like to call our library with Either but may with more sofisticated types, such as IO. Then, how to rely on this recover function anyway, regardless of the type of e?

The answer is in ad-hoc polymorphism. In this case, fortunately, libraries like Arrow in Kotlin and cats in Scala provide such a typeclass: ApplicativeError. Looking closely at ApplicativeError we can see a function called handleErrorWith whose signature is:

def handleErrorWith[A](fa: F[A])(f: E => F[A]): F[A]

This is almost the same as our recover function ! We can use it to implement recoverMissing from earlier for different types.

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

def recoverMissing[A, E, F[_]: ApplicativeError[*[_], E]](
    eff: F[A],
    defaultValue: A
  ) = {
    val AE = implicitly[ApplicativeError[F, E]]
    AE.handleErrorWith(eff)(_ => AE.pure(defaultValue))
  }
val o: Option[String] = Option.empty
val e: Either[Int, String] = Left(42)

recoverMissing(o, "forty two")
// Some(forty two)
recoverMissing(e, "forty two")
// Right(forty two)

The definition of recoverMissing translates to: given an error type E and a type constructor F which has (at least) one parameter and for which we have an implementation of ApplicativeError, we can provide an implementation of recoverMissing. The * notation is provided by the kind-projector plugin which is required to to compile this sample.

If we had paid more attention to the ApplicativeError typeclass, we would have notice the handleError function which is exactly our recoverMissing function.

Conclusion

In this short article, we saw how to deal with error recovery in a functional way. An interesting thing with functional programming is that we can incrementaly improve/generalize a solution.