Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ApplicativeError functions handling and recover...

ApplicativeError functions handling and recovering from errors: A mnemonic to recall their signatures from their names

ApplicativeError functions handling and recovering from errors: A mnemonic to recall their signatures from their names.

With a simple example of function usage for monadic effects Try, Either, Future, IO, and applicative effect ValidatedNel.

Keywords: "applicative effect", "ApplicativeError", "cats", "either monad", "error-handling", "fp", "functional programming", "future monad", "handleerror", "handleerrorwith", "io monad", "MonadError", "monadic effect", "recover", "recoverwith", "try monad", "typelevel-scala", "validatednel"

Avatar for Philip Schwarz

Philip Schwarz PRO

August 11, 2025
Tweet

More Decks by Philip Schwarz

Other Decks in Programming

Transcript

  1. @philip_schwarz slides by https://fpilluminated.org/ MNEMONIC + EXAMPLE trait ApplicativeError[F[_], E]

    extends Applicative[F] : … def handleError[A](fa: F[A])(f: E => A): F[A] def handleErrorWith[A](fa: F[A])(f: E => F[A]): F[A] … def recover[A](fa: F[A])(pf: PartialFunction[E, A]): F[A] def recoverWith[A](fa: F[A])(pf: PartialFunction[E, F[A]]): F[A] … Cats ApplicativeError functions handling and recovering from errors A mnemonic to recall their signatures from their names With a simple example of function usage for monadic effects Try, Either, Future, IO and applicative effect ValidatedNel
  2. The ApplicativeError type class in the Cats library provides many

    useful error-management functions. Among the provided functions, are these two pairs: As we’ll soon see, while all four functions take a function parameter, in one pair, the parameter is a partial function, whereas in the other pair, it is a total function (AKA complete function - see next two slides for a refresher on partial and complete functions). Can you tell, just by looking at the names of the functions, which pair is which? For a period of time I didn’t think so, and every time I forgot the answer, I said to myself that I should come up with some mnemonic to remind myself. The simple aim of this deck is to show you a mnemonic that seems to do the trick, and also to provide an introductory look at the four functions. @philip_schwarz handleError handleErrorWith recover recoverWith /** * An applicative that also allows you to raise and or handle an error value. * * This type class allows one to abstract over error-handling applicatives. */ trait ApplicativeError[F[_], E] extends Applicative[F]
  3. Case sequences as partial functions A sequence of cases (i.e.,

    alternatives) in curly braces can be used anywhere a function literal can be used. Essentially, a case sequence is a function literal, only more general. Instead of having a single entry point and list of parameters, a case sequence has multiple entry points, each with their own list of parameters. Each case is an entry point to the function, and the parameters are specified with the pattern. The body of each entry point is the right-hand side of the case. Here is a simple example: val withDefault: Option[Int] => Int = case Some(x) => x case None => 0 The body of this function has two cases. The first case matches a Some, and returns the number inside the Some. The second case matches a None, and returns a default value of zero. Here is this function in use: withDefault(Some(10)) // 10 withDefault(None) // 0 … One other generalization is worth noting: a sequence of cases gives you a partial function. If you apply such a function on a value it does not support, it will generate a run-time exception. For example, here is a partial function that returns the second element of a list of integers: val second: List[Int] => Int = case x :: y :: _ => y When you compile this, the compiler will correctly warn that the match is not exhaustive: 2 | case x :: y :: _ => y | ˆ | match may not be exhaustive. | | It would fail on pattern case: List(_), Nil This function will succeed if you pass it a three-element list, but not if you pass it an empty list: scala> second(List(5, 6, 7)) val res24: Int = 6
  4. scala> second(List()) scala.MatchError: List() (of class Nil$) at rs$line$10$.$init$$$anonfun$1(rs$line$10:2) at

    rs$line$12$.<init>(rs$line$12:1) If you want to check whether a partial function is defined, you must first tell the compiler that you know you are working with partial functions. The type List[Int] => Int includes all functions from lists of integers to integers, whether or not the functions are partial. The type that only includes partial functions from lists of integers to integers is written PartialFunction[List[Int],Int]. Here is the second function again, this time written with a partial function type: val second: PartialFunction[List[Int],Int] = case x :: y :: _ => y Partial functions have a method isDefinedAt, which can be used to test whether the function is defined at a particular value. In this case, the function is defined for any list that has at least two elements: second.isDefinedAt(List(5,6,7)) // true second.isDefinedAt(List()) // false The typical example of a partial function is a pattern matching function literal like the one in the previous example. In fact, such an expression gets translated by the Scala compiler to a partial function by translating the patterns twice—once for the implementation of the real function, and once to test whether the function is defined or not. For instance, the function literal { case x :: y :: _ => y } gets translated to the following partial function value: new PartialFunction[List[Int], Int]: def apply(xs: List[Int]) = xs match case x :: y :: _ => y def isDefinedAt(xs: List[Int]) = xs match case x :: y :: _ => true case _ => false This translation takes effect whenever the declared type of a function literal is PartialFunction. If the declared type is just Function1, or is missing, the function literal is instead translated to a complete function. In general, you should try to work with complete functions whenever possible, because using partial functions allows for runtime errors that the compiler cannot help you with. Sometimes partial functions are really helpful though. You might be sure that an unhandled value will never be supplied. Alternatively, you might be using a framework that expects partial functions and so will always check isDefinedAt before calling the function. … .
  5. Imagine having just written some code that computes a value

    by following a process consisting of multiple steps, in some of which there is the potential for errors occurring. In order to avoid side effects, you have decided to model errors using a monadic effect. As an example, here is a contrived, trivial program that I have just made up, purely to provide us with some context in which to discuss the four aforementioned ApplicativeError functions (nothing about this program should be treated as exemplary). See the next slide for an explanation of the program. import scala.util.Try object MainTry : def main(args: Array[String]): Unit = println(s"""args="${args.mkString(" ")}"""") val result: Try[String] = divide(args) println(s"result=$result") def divide(args: Array[String]): Try[String] = for firstArgument <- Try(args(0)) secondArgument <- Try(args(1)) dividend <- Try(firstArgument.toInt) divisor <- Try(secondArgument.toInt) quotient <- Try(dividend / divisor) yield s"$dividend/$divisor = $quotient"
  6. When a user runs the program, they are expected to

    supply two integer numbers as arguments. The program displays any and all supplied textual arguments, gets hold of the first and second arguments, parses them into integers, computes their quotient, and finally displays the result of the computation. There are five obvious places where it is expected that an error can occur: • The first argument has not been supplied. • The second argument has not been supplied. • The first argument cannot be parsed into an integer. • The second argument cannot be parsed into an integer. • The quotient cannot be computed because the second argument (the divisor) is zero. To avoid side effects, we have decided to model errors using the Try monadic effect. The fact that the effect is monadic has the downside that the program is unable to report multiple errors: as soon as an error is encountered, the program stops and reports the error. We will improve on this later on by switching to an applicative effect. In the sunny-day scenario, the program computes a Success that wraps a string showing the quotient of the input arguments. In any of the five expected rainy-day scenarios, the program computes a Failure that wraps a Throwable. Similarly for any other rainy-day scenarios. import scala.util.Try object MainTry : def main(args: Array[String]): Unit = println(s"""args="${args.mkString(" ")}"""") val result: Try[String] = divide(args) println(s"result=$result") def divide(args: Array[String]): Try[String] = for firstArgument <- Try(args(0)) secondArgument <- Try(args(1)) dividend <- Try(firstArgument.toInt) divisor <- Try(secondArgument.toInt) quotient <- Try(dividend / divisor) yield s"$dividend/$divisor = $quotient"
  7. Let’s see the program in action. Here is the output

    it produces in the sunny day scenario and in the expected rainy day scenarios. args="10 2" result=Success(10/2 = 5) args="" result=Failure(java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0) args="10" result=Failure(java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1) args="foo 2" result=Failure(java.lang.NumberFormatException: For input string: "foo") args="10 bar" result=Failure(java.lang.NumberFormatException: For input string: "bar") args="10 0" result=Failure(java.lang.ArithmeticException: / by zero)
  8. As we saw earlier, the divide function computes its result

    in a Try monadic context. val result: Try[String] = divide(args) Let’s improve the program so that instead of always printing the result of the divide function, in the case when the result is a Failure wrapping a Throwable, e.g. Failure(java.lang.ArithmeticException: / by zero) the program converts the Failure to a Success wrapping a message indicating that an error has occurred, and showing the Throwable’s error message: Success("There is an error in the input arguments: java.lang.ArithmeticException: / by zero") The way we are going to achieve the above improvement is by invoking on divide(args) one of the following functions provided by ApplicativeError: The reason why we are able to invoke the functions is that (as you can see on the next slide) Cats makes available MonadThrow instance MonadThrow[Try], which is an alias for MonadError[Try, Throwable], which in turn extends ApplicativeError[Try, Throwable]. But which of the two pairs of functions fits our use case? Are we handling an error or recovering from it? To help answer this question, we can use the mnemonic described in the two slides that follow the next one. handleError handleErrorWith recover recoverWith
  9. type MonadThrow[F[_]] = MonadError[F, Throwable] An applicative that also allows

    you to raise and or handle an error value. This type class allows one to abstract over error-handling applicatives. trait ApplicativeError[F[_], E] extends Applicative[F] { … Applicative Monad Functor ApplicativeError MonadError Applicative functor. Allows application of a function in an Applicative context to a value in an Applicative context. See: The Essence of the Iterator Pattern. Also: Applicative programming with effects. Must obey the laws defined in cats.laws.ApplicativeLaws. trait Applicative[F[_]] extends Apply[F] with InvariantMonoidal[F] { self => Apply FlatMap Monad. Allows composition of dependent effectful functions. See: Monads for functional programming Must obey the laws defined in cats.laws.MonadLaws. trait Monad[F[_]] extends FlatMap[F] with Applicative[F] { MonadThrow A monad that also allows you to raise and or handle an error value. This type class allows one to abstract over error-handling monads. trait MonadError[F[_], E] extends ApplicativeError[F, E] with Monad[F] { … type ApplicativeThrow[F[_]] = ApplicativeError[F, Throwable] ApplicativeThrow Cats
  10. Earlier we said that while all four functions take a

    single parameter that is also a function, in one pair, the parameter is a partial function, whereas in the other pair, it is a total function. To remember which pair is which, we follow two steps. In the first step, shown below, we capitalise the second letter of each function and rotate it clockwise by 180 degrees, thereby transforming the letters into logical operators ∀ and ∃. handleError handleErrorWith recover recoverWith hAndleError hAndleErrorWith rEcover rEcoverWith h∀ndleError h∀ndleErrorWith r∃cover r∃coverWith A A E E a A e E
  11. Universal Quantification ∀𝑥𝑃 𝑥 is true when 𝑃 𝑥 is

    true for all 𝑥. Existential Quantification ∃𝑥𝑃 𝑥 is true when 𝑃 𝑥 is true for at least some value of 𝑥. r∃cover r∃coverWith h∀ndleError h∀ndleErrorWith Handle any error, by mapping it to a pure value. Handle any error, by mapping it to an effect. Recover from certain errors by mapping them to a pure value. Recover from certain errors by mapping them to an effect. def recover[A](fa: F[A])(pf: PartialFunction[E, A]): F[A] def recoverWith[A](fa: F[A])(pf: PartialFunction[E, F[A]]): F[A] def handleError[A](fa: F[A])(f: E => A): F[A] def handleErrorWith[A](fa: F[A])(f: E => F[A]): F[A] Now we can just exploit the definition of logical operators ∀ and ∃ to remind us of • which pair of functions is used to handle any and all errors – the parameter of such functions is a total function. • which pair of functions is used to handle only some errors – the parameter of such functions is a partial function. See next slide for the API comment for each of the four functions. As you can see, while the functions passed to handleError and recover return pure value A, the functions passed to handleErrorWith and recoverWith return effect F[A]. In this deck we are going to use handleErrorWith and recoverWith.
  12. /** * Handle any error, by mapping it to an

    `A` value. * * See also handleErrorWith to map to an `F[A]` value * instead of simply an `A` value. * * See also recover to only recover from certain errors. */ def handleError[A](fa: F[A])(f: E => A): F[A] /** * Handle any error, potentially recovering from it, * by mapping it to an `F[A]` value. * * See also handleError to handle any error by simply * mapping it to an `A` value instead of an `F[A]`. * * See also recoverWith to recover from only certain errors. */ def handleErrorWith[A](fa: F[A])(f: E => F[A]): F[A] /** * Recover from certain errors by mapping them to an `A` value. * * See also handleError to handle any/all errors. * * See also recoverWith to recover from certain errors by * mapping them to `F[A]` values. */ def recover[A](fa: F[A])(pf: PartialFunction[E, A]): F[A] = /** * Recover from certain errors by mapping them to an * `F[A]` value. * * See also handleErrorWith to handle any/all errors. * * See also recover to recover from certain errors by * mapping them to `A` values. */ def recoverWith[A](fa: F[A])(pf: PartialFunction[E, F[A]]): F[A] = Cats
  13. Having seen a mnemonic that we can use to determine

    which of the two pairs of ApplicativeError error-management functions fits our use case, let’s get back to our current objective, stated as follows five slides ago: improve the program so that instead of always printing the result of the divide function, in the case when the result is a Failure wrapping a Throwable, e.g. Failure(java.lang.ArithmeticException: / by zero) the program converts the Failure to a Success wrapping a message indicating that an error has occurred, and showing the Throwable’s error message: Success("There is an error in the input arguments: java.lang.ArithmeticException: / by zero") The way we are going to achieve the above improvement is by invoking on divide(args) one of the following functions provided by ApplicativeError: On the next slide we achieve the desired improvement using handleErrorWith. handleError handleErrorWith recover recoverWith
  14. def handle: Throwable => Try[String] = e => Success(s"There is

    an error in the input arguments: $e") val result: Try[String] = divide(args) val result: Try[String] = divide(args).handleErrorWith(handle) args="10 foo" result=Success(There is an error in the input arguments: java.lang.NumberFormatException: For input string: "foo") args="10 foo" result=Failure(java.lang.NumberFormatException: For input string: "foo") import cats.implicits.*
  15. def recover: PartialFunction[Throwable, Try[String]] = case _ : IndexOutOfBoundsException =>

    Success( "ERROR: One or both of the expected input parameters was missing.") case _ : NumberFormatException => Success( "ERROR: One or both of the input parameters was not an integer.") case e : ArithmeticException if e.getMessage == "/ by zero" => Success( "ERROR: The second input parameter was zero.") val result: Try[String] = divide(args).recoverWith(recover) args="10 foo" result=Success(ERROR: One or both of the input parameters was not an integer.) def handle: Throwable => Try[String] = e => Success(s"There is an error in the input arguments: $e") val result: Try[String] = divide(args).handleErrorWith(handle) args="10 foo" result=Success(There is an error in the input arguments: java.lang.NumberFormatException: For input string: "foo") import cats.implicits.*
  16. As we said earlier, the reason why we are able

    to invoke functions handleErrorWith and recoverWith on divide(args), is that Cats makes available MonadThrow instance MonadThrow[Try]. Because MonadThrow functionality is available for all monads, we can very easily change the monadic effect used by our program. To illustrate this, let’s look at the simple changes that are required to switch from Try to each of Either, Future, and IO. Let’s begin by seeing, in the next three slides, how we need to change the original version of our program’s main function when we switch from Try to each of the three chosen alternative monadic effects.
  17. import scala.util.Try object MainTry : def main(args: Array[String]): Unit =

    println(s"""args="${args.mkString(" ")}"""") val result: Try[String] = divide(args) println(s"result=$result") def divide(args: Array[String]): Try[String] = for firstArgument <- Try(args(0)) secondArgument <- Try(args(1)) dividend <- Try(firstArgument.toInt) divisor <- Try(secondArgument.toInt) quotient <- Try(dividend / divisor) yield s"$dividend/$divisor = $quotient" import scala.util.Try object MainEither : def main(args: Array[String]): Unit = println(s"""args="${args.mkString(" ")}"""") val result: ErrorOr[String] = divide(args) println(s"result=$result") def divide(args: Array[String]): ErrorOr[String] = for firstArgument <- Try(args(0)).toEither secondArgument <- Try(args(1)).toEither dividend <- Try(firstArgument.toInt).toEither divisor <- Try(secondArgument.toInt).toEither quotient <- Try(dividend / divisor).toEither yield s"$dividend/$divisor = $quotient” type ErrorOr[A] = Either[Throwable, A] Try[String] Either[Throwable, String]
  18. import scala.concurrent.{Await, Future} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.Duration object MainFuture :

    def main(args: Array[String]): Unit = println(s"""args="${args.mkString(" ")}"""") val result: Future[String] = Await.ready(divide(args),Duration.Inf) println(s"result=$result") def divide(args: Array[String]): Future[String] = for firstArgument <- Future(args(0)) secondArgument <- Future(args(1)) dividend <- Future(firstArgument.toInt) divisor <- Future(secondArgument.toInt) quotient <- Future(dividend / divisor) yield s"$dividend/$divisor = $quotient" import scala.util.Try object MainTry : def main(args: Array[String]): Unit = println(s"""args="${args.mkString(" ")}"""") val result: Try[String] = divide(args) println(s"result=$result") def divide(args: Array[String]): Try[String] = for firstArgument <- Try(args(0)) secondArgument <- Try(args(1)) dividend <- Try(firstArgument.toInt) divisor <- Try(secondArgument.toInt) quotient <- Try(dividend / divisor) yield s"$dividend/$divisor = $quotient" Try[String] Future[String]
  19. import cats.effect.IO import cats.effect.unsafe.implicits.global object MainIO : def main(args: Array[String]):

    Unit = println(s"""args="${args.mkString(" ")}"""") val result: IO[String] = divide(args) println(result.unsafeRunSync()) def divide(args: Array[String]): IO[String] = for firstArgument <- IO(args(0)) secondArgument <- IO(args(1)) dividend <- IO(firstArgument.toInt) divisor <- IO(secondArgument.toInt) quotient <- IO(dividend / divisor) yield s"$dividend/$divisor = $quotient" import scala.util.Try object MainTry : def main(args: Array[String]): Unit = println(s"""args="${args.mkString(" ")}"""") val result: Try[String] = divide(args) println(s"result=$result") def divide(args: Array[String]): Try[String] = for firstArgument <- Try(args(0)) secondArgument <- Try(args(1)) dividend <- Try(firstArgument.toInt) divisor <- Try(secondArgument.toInt) quotient <- Try(dividend / divisor) yield s"$dividend/$divisor = $quotient" Try[String] IO[String]
  20. What we need to do next, is change the main

    function variant for each monadic effect, so that it uses the handleErrorWith function. This first very simple change is shown on the next slide. But we also need to provide, for each of Either, Future, and IO, a dedicated variant of the handle function passed to handleErrorWith. Because doing so would be very repetitive, let’s take a shortcut and instead change the original handle function so that rather than just working for a particular monadic effect, it works for any applicative effect. In other words, instead of having one variant of the handle function that lifts an error message into the Try monadic effect, another variant that lifts it into the Either monadic effect, and so on, for each monadic effect, let’s just have a single handle function that lifts the error message into any applicative effect (and therefore into any monadic effect). This second change is shown on the slide after next. The subsequent slide shows how the above two changes affect the output of all our variants of the program.
  21. val result: Try[String] = divide(args) val result: ErrorOr[String] = divide(args)

    val result: Future[String] = Await.ready( divide(args), Duration.Inf ) val result: IO[String] = divide(args) val result: Try[String] = divide(args).handleErrorWith(handle) val result: ErrorOr[String] = divide(args).handleErrorWith(handle) val result: Future[String] = Await.ready( divide(args).handleErrorWith(handle), Duration.Inf ) val result: IO[String] = divide(args).handleErrorWith(handle)
  22. def handle[F[_]](using ae: ApplicativeError[F,Throwable]): Throwable => F[String] = e =>

    ae.pure(s"There is an error in the input arguments: $e") def handle: Throwable => Try[String] = e => Success(s"There is an error in the input arguments: $e")
  23. args="10 foo" result=Success(There is an error in the input arguments:

    java.lang.NumberFormatException: For input string: "foo") args="10 foo" result=Failure(java.lang.NumberFormatException: For input string: "foo") args="10 foo" result=Right(There is an error in the input arguments: java.lang.NumberFormatException: For input string: "foo") args="10 foo" result=Left(java.lang.NumberFormatException: For input string: "foo") args="10 foo" result= Future(Success(There is an error in the input arguments: java.lang.NumberFormatException: For input string: "foo")) args="10 foo" result=Future(Failure(java.lang.NumberFormatException: For input string: "foo")) args="10 foo" Exception in thread "main" java.lang.NumberFormatException: For input string: "foo” … args="10 foo" result=There is an error in the input arguments: java.lang.NumberFormatException: For input string: "foo" Try Either Future IO Try Either Future IO divide(args).handleErrorWith(handle) divide(args)
  24. val result: Try[String] = divide(args) val result: ErrorOr[String] = divide(args)

    val result: Future[String] = Await.ready( divide(args), Duration.Inf ) val result: IO[String] = divide(args) val result: ErrorOr[String] = divide(args).recoverWith(recover) val result: Future[String] = Await.ready( divide(args).recoverWith(recover), Duration.Inf ) val result: IO[String] = divide(args).recoverWith(recover) val result: Try[String] = divide(args).recoverWith(recover)
  25. def recover[F[_]](using ae: ApplicativeError[F,Throwable]): PartialFunction[Throwable, F[String]] = case _ :

    IndexOutOfBoundsException => ae.pure("ERROR: One or both of the expected input parameters was missing.") case _ : NumberFormatException => ae.pure("ERROR: One or both of the input parameters was not an integer.") case e : ArithmeticException if e.getMessage == "/ by zero" => ae.pure("ERROR: The second input parameter was zero.") def recover: PartialFunction[Throwable, Try[String]] = case _ : IndexOutOfBoundsException => Success("ERROR: One or both of the expected input parameters was missing.") case _ : NumberFormatException => Success("ERROR: One or both of the input parameters was not an integer.") case e : ArithmeticException if e.getMessage == "/ by zero" => Success("ERROR: The second input parameter was zero.")
  26. args="10 foo" result=Success(ERROR: One or both of the input parameters

    was not an integer.) args="10 foo" result=Failure(java.lang.NumberFormatException: For input string: "foo") args="10 foo" result=Right(ERROR: One or both of the input parameters was not an integer.) args="10 foo" result=Left(java.lang.NumberFormatException: For input string: "foo") args="10 foo" result=Future(Success(ERROR: One or both of the input parameters was not an integer.)) args="10 foo" result=Future(Failure(java.lang.NumberFormatException: For input string: "foo")) args="10 foo" Exception in thread "main" java.lang.NumberFormatException: For input string: "foo” … args="10 foo" result=ERROR: One or both of the input parameters was not an integer." Try Either Future IO Try Either Future IO divide(args).recoverWith(recover) divide(args)
  27. To avoid side effects, we modeled errors using a monadic

    effect. The specific monadic effects that we used were Try, Either, Future and IO. As mentioned at the beginning of this deck, using monadic effects has the downside that the program is unable to report multiple errors: as soon as an error is encountered, the program stops and reports the error. In order to avoid the above downside, let’s change the original program so that instead of using the Try monadic effect, it uses the ValidatedNel† applicative effect provided by Cats, so that the program is able to report multiple errors. See next slide for the required changes, and an example of how they improve error reporting. If you want to know more about the Applicative type class, see the decks listed on the last slide. † ValidatedNel[E, A] is just an alias for Validated[NonEmptyList[E], A]
  28. import scala.util.Try object MainTry : def main(args: Array[String]): Unit =

    println(s"""args="${args.mkString(" ")}"""") val result: Try[String] = divide(args) println(s"result=$result") def divide(args: Array[String]): Try[String] = for firstArgument <- Try(args(0)) secondArgument <- Try(args(1)) dividend <- Try(firstArgument.toInt) divisor <- Try(secondArgument.toInt) quotient <- Try(dividend / divisor) yield s"$dividend/$divisor = $quotient" import cats.data.{NonEmptyList, Validated, ValidatedNel} import scala.util.Try object MainValidated : def main(args: Array[String]): Unit = println(s"""args="${args.mkString(" ")}"""") val result: ValidatedNel[Throwable, String] = divide(args) println(s"result=$result") def divide(args: Array[String]): ValidatedNel[Throwable], String] = ( Try(args(0)).toValidated.andThen { firstArgument => Try(firstArgument.toInt).toValidated }.toValidatedNel , Try(args(1)).toValidated.andThen { secondArgument => Try(secondArgument.toInt).toValidated.andThen (n => Validated.cond( test = n != 0, a = n, e = IllegalArgumentException("second argument is zero") ) ) }.toValidatedNel ) .mapN( (dividend, divisor) => s"$dividend/$divisor = ${dividend / divisor}" ) args="foo 0" result=Failure( java.lang.NumberFormatException: For input string: "foo") args="foo 0" result=Invalid(NonEmptyList( java.lang.NumberFormatException: For input string: "foo", java.lang.IllegalArgumentException: second argument is zero)) Try[String] ValidatedNel[Throwable,[String]
  29. def divide(args: Array[String]): ValidatedNel[Throwable], String] = (Try(args(0)).toValidated.andThen { firstArgument =>

    Try(firstArgument.toInt).toValidated }.toValidatedNel, Try(args(1)).toValidated.andThen { secondArgument => Try(secondArgument.toInt).toValidated.andThen (n => Validated.cond(n != 0, n, IllegalArgumentException("second argument is zero"))) }.toValidatedNel ).mapN( (dividend, divisor) => s"$dividend/$divisor = ${dividend / divisor}") Same divide function as on the previous slide, but more compactly formatted.
  30. Now let’s make the same kind of error handling improvement

    that we made when using monadic effects, by exploiting functions recoverWith and handleErrorWith. The two functions are provided by ApplicativeError, and an instance of ValidatedNel is an applicative effect, i.e. Cats is able to provide an instance of ApplicativeError for it, so it is perfectly possible to invoke the two functions on expression divide(args). But while it can make some sense to use recoverWith and handleErrorWith to convert a Try/Either/Future/IO of a failed computation into a Try/Either/Future/IO of a successful computation of a string informing the user of an error (see example four slides ago), the same is not true for ValidatedNel, because the errors that we are dealing with are validation errors, and so it does not make sense to turn a failed validation into a successful validation. Instead, see the next slide for how, now that we are using the ValidatedNel applicative effect, we can achieve a similar improvement to the one that we achieved by exploiting recoverWith and handleErrorWith when we used monadic effects.
  31. val result: Validated[NonEmptyList[Throwable],String] = divide(args) val result: String = divide(args).valueOr(handleErrors)

    args="foo 0" result=Invalid(NonEmptyList( java.lang.NumberFormatException: For input string: "foo", java.lang.IllegalArgumentException: second argument is zero)) def handleErrors(throwables: NonEmptyList[Throwable]): String = throwables.toList.map : case e: IndexOutOfBoundsException => s"ERROR: An expected input parameters was missing: $e." case e: NumberFormatException => s"ERROR: An input parameter was not an integer: $e." case e: IllegalArgumentException if e.getMessage == "second argument is zero" => s"ERROR: The second input parameter was zero." case e => s"UNEXPECTED ERROR: $e." match case errorMessages => s"""\n\tThere were one or more errors in the input parameters:\n\t\t${errorMessages.mkString("\n\t\t")}""" args="foo 0" result=" There were one or more errors in the input parameters: ERROR: An input parameter was not an integer: java.lang.NumberFormatException: For input string: "foo". ERROR: The second input parameter was zero."
  32. That’s all for this deck. I hope you found it

    useful. If you could do with an introduction to the Applicative type class, see the decks on the next slide.