Functional Error Handling

Functional Error Handling

Exceptions in OOP centric langs such as Java are abused for control flow and event signaling.
Lack of proper support for Monads, Higher Kinded Types and other facilities leave lang users with no alternative but to choose happy paths as return types of method signatures.

In this talk we will cover some examples regarding the misuse of exceptions and proper data types such as `Option`, `Try`, `Either[E, A]` and `MonadError[M[_], E]` to model absence of values, failing computations and alternate paths in method return types.

https://github.com/47deg/functional-error-handling

Ad2476bf0540dfaa0fc30cb62c8e07da?s=128

Raúl Raja Martínez

February 17, 2017
Tweet

Transcript

  1. FUNCTIONAL ERROR HANDLING @raulraja @47deg 2 Í

  2. Functional Error Handling 3 Í

  3. Requirements 1. Arm a Nuke launcher 2. Aim toward a

    Target 3. Launch a Nuke and impact the Target 4 Í
  4. Requirements 1. arm a Nuke launcher 2. aim toward a

    Target 3. launch a Nuke and Impact the target 5 Í
  5. Requirements /** model */ case class Nuke() case class Target()

    case class Impacted() def arm: Nuke = ??? def aim: Target = ??? def launch(target: Target, nuke: Nuke): Impacted = ??? 6 Í
  6. Exceptions def arm: Nuke = throw new RuntimeException("SystemOffline") def aim:

    Target = throw new RuntimeException("RotationNeedsOil") def launch(target: Target, nuke: Nuke): Impacted = Impacted() 7 Í
  7. Exceptions Breaks Referential transparency def arm: Nuke = throw new

    RuntimeException("SystemOffline") def aim: Target = throw new RuntimeException("RotationNeedsOil") def launch(target: Target, nuke: Nuke): Impacted = Impacted() 8 Í
  8. Exceptions: Broken GOTO They are a broken GOTO def arm:

    Nuke = throw new RuntimeException("SystemOffline") def aim: Target = throw new RuntimeException("RotationNeedsOil") def launch(target: Target, nuke: Nuke): Impacted = Impacted() 9 Í
  9. Exceptions: Broken GOTO They are a broken GOTO getting lost

    in async boundaries def arm: Nuke = throw new RuntimeException("SystemOffline") def aim: Target = throw new RuntimeException("RotationNeedsOil") def launch(target: Target, nuke: Nuke): Impacted = Impacted() def attack: Future[Impacted] = Future(launch(arm, aim)) 10 Í
  10. Exceptions Abused to signal events in core libraries at java.lang.Throwable.fillInStackTrace(Throwable.java:-1)

    at java.lang.Throwable.fillInStackTrace(Throwable.java:782) - locked <0x6c> (a sun.misc.CEStreamExhausted) at java.lang.Throwable.<init>(Throwable.java:250) at java.lang.Exception.<init>(Exception.java:54) at java.io.IOException.<init>(IOException.java:47) at sun.misc.CEStreamExhausted.<init>(CEStreamExhausted.java:30) at sun.misc.BASE64Decoder.decodeAtom(BASE64Decoder.java:117) at sun.misc.CharacterDecoder.decodeBuffer(CharacterDecoder.java:163) at sun.misc.CharacterDecoder.decodeBuffer(CharacterDecoder.java:194) 11 Í
  11. Exceptions Unsealed hierarchies, root of all evil try { doExceptionalStuff()

    //throws IllegalArgumentException } catch (Throwable e) { //too broad matches: /* VirtualMachineError OutOfMemoryError ThreadDeath LinkageError InterruptedException ControlThrowable NotImplementedError */ } 12 Í
  12. Exceptions Potentially costly to construct based on VM impl and

    your current Thread stack size public class Throwable { /** * Fills in the execution stack trace. * This method records within this Throwable object information * about the current state of the stack frames for the current thread. */ Throwable fillInStackTrace() } 13 Í
  13. Exceptions New: Creating a new Throwable each time Lazy: Reusing

    a created Throwable in the method invocation. Static: Reusing a static Throwable with an empty stacktrace. The Hidden Performance costs of instantiating Throwables Í
  14. Exceptions Poor choices when using exceptions Modeling absence Modeling known

    business cases that result in alternate paths Async boundaries over unprincipled APIs (callbacks) When people have no access to your source code 15 Í
  15. Exceptions Maybe OK if You don't expect someone to recover

    from it You are contributor to a JVM in JVM internals You want to create chaos and mayhem to overthrow the government In this talk You know what you are doing 16 Í
  16. How do we model exceptional cases then? 17 Í

  17. MONADS! 18 Í

  18. Option When modeling the potential absence of a value 19

    Í
  19. Option When modeling the potential absence of a value sealed

    trait Option[+A] case class Some[+A](value: A) extends Option[A] case object None extends Option[Nothing] 20 Í
  20. Option Useful combinators Garbage def fold[B](ifEmpty: B)(f: (A) B): B

    //inspect all paths def map[B](f: (A) B): Option[B] //transform contents def flatMap[B](f: (A) Option[B]): Option[B] //monadic bind to another option def filter(p: (A) Boolean): Option[A] //filter with predicate def getOrElse[B >: A](default: B): B //extract or provide alternative def get: A //NoSuchElementException if empty ( °□° 21 Í
  21. Option How would our example look like? def arm: Option[Nuke]

    = None def aim: Option[Target] = None def launch(target: Target, nuke: Nuke): Option[Impacted] = Some(Impacted()) 22 Í
  22. Option Pain to deal with if your lang does not

    have proper Monads or syntax support def attackImperative: Option[Impacted] = { var impact: Option[Impacted] = None val optionNuke = arm if (optionNuke.isDefined) { val optionTarget = aim if (optionTarget.isDefined) { impact = launch(optionTarget.get, optionNuke.get) } } impact } 23 Í
  23. Option Easy to work with if your lang supports monad

    comprehensions or special syntax def attackMonadic: Option[Impacted] = for { nuke <- arm target <- aim impact <- launch(target, nuke) } yield impact 24 Í
  24. Try When a computation may fail with a runtime exception

    25 Í
  25. Try When a computation may fail with a runtime exception

    sealed trait Try[+T] case class Failure[+T](exception: Throwable) extends Try[T] case class Success[+T](value: T) extends Try[T] 26 Í
  26. Try Useful combinators Garbage def fold[U](fa: (Throwable) U, fb: (T)

    U): U //inspect all paths def map[U](f: (T) U): Try[U] //transform contents def flatMap[U](f: (T) Try[U]): Try[U] //monadic bind to another Try def filter(p: (T) Boolean): Try[T] //filter with predicate def getOrElse[U >: T](default: U): U // extract the value or provide an alternative if def get: T //throws the captured exception if not a Success ( °□° 27 Í
  27. Try How would our example look like? def arm: Try[Nuke]

    = Try(throw new RuntimeException("SystemOffline")) def aim: Try[Target] = Try(throw new RuntimeException("RotationNeedsOil")) def launch(target: Target, nuke: Nuke): Try[Impacted] = Try(throw new RuntimeException("MissedByMeters")) 28 Í
  28. Try Pain to deal with if your lang does not

    have proper Monads or syntax support def attackImperative: Try[Impacted] = { var impact: Try[Impacted] = null var ex: Throwable = null val tryNuke = arm if (tryNuke.isSuccess) { val tryTarget = aim if (tryTarget.isSuccess) { impact = launch(tryTarget.get, tryNuke.get) } else { ex = tryTarget.failed.get } } else { ex = tryNuke.failed.get } if (impact != null) impact else Try(throw ex) } 29 Í
  29. Try Easy to work with if your lang supports monadic

    comprehensions def attackMonadic: Try[Impacted] = for { nuke <- arm target <- aim impact <- launch(target, nuke) } yield impact 30 Í
  30. Either When dealing with a known alternate return path 31

    Í
  31. Either When a computation may fail or dealing with known

    alternate return path sealed abstract class Either[+A, +B] case class Left[+A, +B](value: A) extends Either[A, B] case class Right[+A, +B](value: B) extends Either[A, B] 32 Í
  32. Either Useful combinators Garbage def fold[C](fa: (A) C, fb: (B)

    C): C //inspect all paths def map[Y](f: (B) Y): Either[A, Y] //transform contents def flatMap[AA >: A, Y](f: (B) Either[AA, Y]): Either[AA, Y] //monadic bind if Right def filterOrElse[AA >: A](p: (B) Boolean, zero: AA): Either[AA, B] //filter with pred def getOrElse[BB >: B](or: BB): BB // extract the value or provide an alternative if a toOption.get, toTry.get //Looses information if not a Right ( °□° 33 Í
  33. Either What goes on the Left? def arm: Either[?, Nuke]

    = ??? def aim: Either[?,Target] = ??? def launch(target: Target, nuke: Nuke): Either[?, Impacted] = ??? 34 Í
  34. Either Alegbraic Data Types (sealed families) sealed trait NukeException case

    class SystemOffline() extends NukeException case class RotationNeedsOil() extends NukeException case class MissedByMeters(meters : Int) extends NukeException 35 Í
  35. Either Algebraic data types (sealed families) def arm: Either[SystemOffline, Nuke]

    = Right(Nuke()) def aim: Either[RotationNeedsOil,Target] = Right(Target()) def launch(target: Target, nuke: Nuke): Either[MissedByMeters, Impacted] = Left(MissedByMe 36 Í
  36. Either Pain to deal with if your lang does not

    have proper Monads or syntax support def attackImperative: Either[NukeException, Impacted] = { var result: Either[NukeException, Impacted] = null val eitherNuke = arm if (eitherNuke.isRight) { val eitherTarget = aim if (eitherTarget.isRight) { result = launch(eitherTarget.toOption.get, eitherNuke.toOption.get) } else { result = Left(RotationNeedsOil()) } } else { result = Left(SystemOffline()) } result } 37 Í
  37. Either Easy to work with if your lang supports monadic

    comprehensions def attackMonadic: Either[NukeException, Impacted] = for { nuke <- arm target <- aim impact <- launch(target, nuke) } yield impact 38 Í
  38. Can we further generalize error handling and launch nukes on

    any M[_]? 39 Í
  39. (Monad|Applicative)Error[M[_], E] /** * 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] { ... } 40 Í
  40. (Monad|Applicative)Error[M[_], E] Many useful methods to deal with potentially failed

    monads def raiseError[A](e: E): F[A] def handleError[A](fa: F[A])(f: E => A): F[A] def attempt[A](fa: F[A]): F[Either[E, A]] def attemptT[A](fa: F[A]): EitherT[F, E, A] def recover[A](fa: F[A])(pf: PartialFunction[E, A]): F[A] def catchNonFatal[A](a: => A)(implicit ev: Throwable <:< E): F[A] def catchNonFatalEval[A](a: Eval[A])(implicit ev: Throwable <:< E): F[A] 41 Í
  41. (Monad|Applicative)Error[M[_], E] Cats instances available for MonadError[Option, Unit] MonadError[Try, Throwable]

    MonadError[Either[E, ?], E] 42 Í
  42. (Monad|Applicative)Error[M[_], E] How can we generalize and implement this to

    any M[_]? def arm: M[Nuke] = ??? def aim: M[Target] = ??? def launch(target: Target, nuke: Nuke): M[Impacted] = ??? 43 Í
  43. (Monad|Applicative)Error[M[_], E] Higher Kinded Types! def arm[M[_]]: M[Nuke] = ???

    def aim[M[_]]: M[Target] = ??? def launch[M[_]](target: Target, nuke: Nuke): M[Impacted] = ??? 44 Í
  44. (Monad|Applicative)Error[M[_], E] Typeclasses! import cats._ import cats.implicits._ def arm[M[_] :

    NukeMonadError]: M[Nuke] = Nuke().pure[M] def aim[M[_] : NukeMonadError]: M[Target] = Target().pure[M] def launch[M[_] : NukeMonadError](target: Target, nuke: Nuke): M[Impacted] = (MissedByMeters(5000): NukeException).raiseError[M, Impacted] 45 Í
  45. (Monad|Applicative)Error[M[_], E] An abstract program is born def attack[M[_] :

    NukeMonadError]: M[Impacted] = (aim[M] |@| arm[M]).tupled.flatMap((launch[M] _).tupled) 46 Í
  46. (Monad|Applicative)Error[M[_], E] Provided there is an instance of MonadError[M[_], A]

    for other types you abstract away the return type attack[Either[NukeException, ?]] attack[Future[Either[NukeException, ?]]] 47 Í
  47. Abstraction Benefits Safer code Less tests More runtime choices Issues

    Performance cost? Newbies & OOP dogmatics complain about legibility Advanced types + inference higher compile times 48 Í
  48. Recap Error Handling When to use Java Kotlin Scala Exceptions

    ~Never x x x Option Modeling Absence ? x x Try Capturing Exceptions ? ? x Either Modeling Alternate Paths ? ? x MonadError Abstracting away concerns - - x 49 Í
  49. Recap What if my lang does not support some of

    these things? 1. Build it yourself 2. Ask lang designers to include HKTs, Typeclasses, ADT and others 3. We are part of the future of programming 50 Í
  50. Thanks! @raulraja @47deg 51 Í