Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Functional Error Handling with Cats

57 views

Published on

Sometimes we want to keep or collect errors for later examination. We’ll use Cats to help us pick the story we want to tell when handling errors; accumulated errors or first error wins. Monads will be included!

Published in: Software
  • Be the first to comment

  • Be the first to like this

Functional Error Handling with Cats

  1. 1. FUNCTIONALFUNCTIONAL ERROR HANDLINGERROR HANDLING WITH CATSWITH CATS MARK CANLASMARK CANLAS ·· NE SCALA 2020NE SCALA 2020
  2. 2. OPTIONOPTION[_][_] TRYTRY[_][_] EITHEREITHER[_, _][_, _]
  3. 3. FUNCTORFUNCTOR[_][_] APPLICATIVEAPPLICATIVE[_][_] MONADMONAD[_][_]
  4. 4. ERRORS ASERRORS AS DATADATA
  5. 5. PARSING STRINGSPARSING STRINGS case class AncientRecord( age: String, name: String, rateOfIncome: String ) case class Citizen( age: Int, name: String, rateOfIncome: Double ) def parseRecord(rec: AncientRecord): Citizen
  6. 6. PARSING FUNCTIONSPARSING FUNCTIONS def parseAge(s: String) def parseName(s: String) def parseRate(s: String)
  7. 7. AA =>  => FF[[BB]] case class AncientRecord( age: String, name: String, rateOfIncome: String ) case class Citizen( age: Int, name: String, rateOfIncome: Double ) // fate of record not guaranteed, via F def parseRecord(rec: AncientRecord): F[Citizen]
  8. 8. OptionOption[_][_] def parseAge(s: String): Option[Int] def parseName(s: String): Option[String] def parseRate(s: String): Option[Double]
  9. 9. FOR COMPREHENSIONFOR COMPREHENSION def parseRecord(rec: AncientRecord): Option[Citizen] = for { age <- parseAge(rec.age) name <- parseName(rec.name) rate <- parseRate(rec.rate) } yield Citizen(age, name, rate)
  10. 10. PARSING WITH OPTIONPARSING WITH OPTION def parseRecord(rec: AncientRecord): Option[Citizen] = for { age <- parseAge(rec.age) // Some[Int] name <- parseName(rec.name) // Some[String] rate <- parseRate(rec.rate) // Some[Double] } yield Citizen(age, name, rate) // Some[Citizen]
  11. 11. NOT ALL AVAILABLENOT ALL AVAILABLE def parseRecord(rec: AncientRecord): Option[Citizen] = for { age <- parseAge(rec.age) // Some[Int] name <- parseName(rec.name) // None rate <- parseRate(rec.rate) // (does not run) } yield Citizen(age, name, rate) // None
  12. 12. FIRST ONE WINSFIRST ONE WINS
  13. 13. FIRST ERROR WINSFIRST ERROR WINS
  14. 14. TryTry[_][_] def parseAge(s: String): Try[Int] def parseName(s: String): Try[String] def parseRate(s: String): Try[Double]
  15. 15. PARSING WITH TRYPARSING WITH TRY def parseRecord(rec: AncientRecord): Try[Citizen] = for { age <- parseAge(rec.age) // Success[Int] name <- parseName(rec.name) // Failure[String] rate <- parseRate(rec.rate) // (does not run) } yield Citizen(age, name, rate) // Failure[Citizen]
  16. 16. Name Happy path Sad path Option[_] Some[_] None Try[_] Success[_] Failure[_]
  17. 17. ERROR ADTERROR ADT sealed trait ParsingErr case class UnreadableAge (s: String) extends ParsingErr case class UnreadableName(s: String) extends ParsingErr case class UnreadableRate(s: String) extends ParsingErr
  18. 18. EitherEither[_, _][_, _] def parseAge(s: String): Either[UnreadableAge, Int] def parseName(s: String): Either[UnreadableName, String] def parseRate(s: String): Either[UnreadableRate, Double]
  19. 19. PARSING WITH EITHERPARSING WITH EITHER def parseRecord(rec: AncientRecord): Either[ParsingErr, Citizen] = for { age <- parseAge(rec.age) // Right[UnreadableAge, Int] name <- parseName(rec.name) // Left[UnreadableName, String] rate <- parseRate(rec.rate) // (does not run) } yield Citizen(age, name, rate) // Left[ParsingErr, Citizen]
  20. 20. Name Happy path Sad path Option[_] Some[_] None Try[_] Success[_] Failure[_] Either[E, _] Right[E, _] Left[E, _]
  21. 21. def parseRecord(rec: AncientRecord): Either[ParsingErr, Citizen]
  22. 22. def parseRecord(rec: AncientRecord): Either[ParsingErr, Citizen] sealed trait HttpResponse case class HttpSucccess (s: String) extends HttpResponse case class HttpClientError(s: String) extends HttpResponse def present(res: Either[ParsingErr, Citizen]): HttpResponse
  23. 23. def parseRecord(rec: AncientRecord): Either[ParsingErr, Citizen] sealed trait HttpResponse case class HttpSucccess (s: String) extends HttpResponse case class HttpClientError(s: String) extends HttpResponse def present(res: Either[ParsingErr, Citizen]): HttpResponse def httpError(err: ParsingError): HttpClientError def httpSuccess(c: Citizen): HttpSucccess
  24. 24. USINGUSING .fold().fold() val res: Either[ParsingErr, Citizen] = ??? val httpResponse: HttpResponse = res .fold(httpError, httpSuccess)
  25. 25. USINGUSING .leftMap().leftMap() val res: Either[ParsingErr, Citizen] = ??? val httpResponse: HttpResponse = res // Either[ParsingErr, Citizen] .map(httpSuccess) // Either[ParsingErr, HttpSucccess] .leftMap(httpError) // Either[HttpClientError, HttpSucccess] .merge // HttpResponse
  26. 26. EitherEither[[EE, , AA]] BIFUNCTORBIFUNCTOR Can map over E Can map over A
  27. 27. POSTFIX SYNTAXPOSTFIX SYNTAX import cats.implicits._ def luckyNumber: Either[String, Int] = if (Random.nextBoolean) 42.asRight else "You are unlucky.".asLeft
  28. 28. IOIO[_][_] def parseAge(s: String): IO[Int] def parseName(s: String): IO[String] def parseRate(s: String): IO[Double]
  29. 29. PARSING WITH IOPARSING WITH IO def parseRecord(rec: AncientRecord): IO[Citizen] = for { age <- parseAge(rec.age) // successful name <- parseName(rec.name) // error "raised" rate <- parseRate(rec.rate) // (does not run) } yield Citizen(age, name, rate) // error raised in step 2
  30. 30. Name Happy path Sad path Option[_] Some[_] None Try[_] Success[_] Failure[_] Either[E, _] Right[E, _] Left[E, _] IO[_] IO[_] IO[_]
  31. 31. MONAD TYPECLASSMONAD TYPECLASS import cats._ import cats.effect._ import scala.util._ type EitherParsingErr[A] = Either[ParsingErr, A] implicitly[Monad[Option]] implicitly[Monad[Try]] implicitly[Monad[EitherParsingErr]] implicitly[Monad[IO]]
  32. 32. ERROR CHANNELSERROR CHANNELS F[A] shape Payload Error channel Try[A] A Throwable IO[A] A Throwable Either[E, A] A E
  33. 33. MONADERROR TYPECLASSMONADERROR TYPECLASS import cats._ import cats.effect._ import scala.util._ type EitherParsingErr[A] = Either[ParsingErr, A] implicitly[MonadError[Try, Throwable]] implicitly[MonadError[IO, Throwable]] implicitly[MonadError[EitherParsingErr, ParsingErr]]
  34. 34. TYPECLASS TABLETYPECLASS TABLE Functor[_]   Applicative[_]   Monad[_] MonadError[_, _]
  35. 35. PARSING WITH EITHERPARSING WITH EITHER def parseRecord(rec: AncientRecord): Either[ParsingErr, Citizen] = for { age <- parseAge(rec.age) // Left[ParsingErr, Int] name <- parseName(rec.name) // (does not run) rate <- parseRate(rec.rate) // (does not run) } yield Citizen(age, name, rate) // only reports first error
  36. 36. GIVEN:GIVEN: EitherEither[[EE, , AA]]
  37. 37. GIVEN:GIVEN: EitherEither[[EE, , AA]] WANT:WANT: EitherEither[[ListList[[EE], ], AA]]
  38. 38. MONADS START WITH:MONADS START WITH: EitherEither[[EE, , AA]]
  39. 39. MONADS START WITH:MONADS START WITH: EitherEither[[EE, , AA]] MONADS END WITH:MONADS END WITH: EitherEither[[EE, , AA]]
  40. 40. ValidatedValidated[_, _][_, _]
  41. 41. ValidatedValidated[_, _][_, _] def parseAge(s: String): Validated[UnreadableAge, Int] def parseName(s: String): Validated[UnreadableName, String] def parseRate(s: String): Validated[UnreadableRate, Double]
  42. 42. PARSING WITH VALIDATED???PARSING WITH VALIDATED??? def parseRecord(rec: AncientRecord): ???[Citizen] = for { age <- ??? // ??? name <- ??? // ??? rate <- ??? // ??? } yield Citizen(age, name, rate) // ???
  43. 43. ACCUMULATIONACCUMULATION def combine( v1: Validated[ParsingErr, Int], v2: Validated[ParsingErr, String]): Validated[List[ParsingError], (Int, String)] = (v1, v2) match { case (Valid(n), Valid(s)) => Valid((n, s)) case (Invalid(err), Valid(s)) => Invalid(List(err)) case (Valid(n), Invalid(err)) => Invalid(List(err)) case (Invalid(e1), Invalid(e2)) => Invalid(List(e1, e2)) }
  44. 44. EitherEither[[EE, , AA]] ValidatedValidated[[EE, , AA]]
  45. 45. VALIDATED AND EITHERVALIDATED AND EITHER Used for capturing errors as data "Exclusive disjunction" (payload A or error E but never both) Bifunctor over A and E
  46. 46. Name Happy path Sad path Option[_] Some[_] None Try[_] Success[_] Failure[_] Either[E, _] Right[E, _] Left[E, _] IO[_] IO[_] IO[_] Validated[E, _] Valid[_] Invalid[E]
  47. 47. ERROR CHANNELSERROR CHANNELS F[A] shape Payload Error channel Try[A] A Throwable IO[A] A Throwable Either[E, A] A E Validated[E, A] A E
  48. 48. APPLICATIVEERROR TYPECLASSAPPLICATIVEERROR TYPECLASS import cats._ import cats.effect._ type EitherParsingErr[A] = Either[ParsingErr, A] type ValidatedParsingErr[A] = Validated[ParsingErr, A] implicitly[ApplicativeError[IO, Throwable]] implicitly[ApplicativeError[EitherParsingErr, ParsingErr]] implicitly[ApplicativeError[ValidatedParsingErr, ParsingErr]] // does not compile implicitly[MonadError[ValidatedParsingErr, ParsingErr]]
  49. 49. TYPECLASS TABLETYPECLASS TABLE Functor[_]   Applicative[_] ApplicativeError[_, _] Monad[_] MonadError[_, _]
  50. 50. POSTFIX SYNTAXPOSTFIX SYNTAX import cats.implicits._ def luckyNumber: Validated[String, Int] = if (Random.nextBoolean) 42.valid else "You are unlucky.".invalid
  51. 51. ValidatedValidated[[EE, , AA]] ValidatedValidated[[EE, , AA]] ValidatedValidated[[EE, , AA]]    ValidatedValidated[[ListList[[EE], ], AA]]
  52. 52. EitherEither[[EE, , AA]] EitherEither[[EE, , AA]] EitherEither[[EE, , AA]]    EitherEither[[EE, , AA]]
  53. 53. ValidatedValidated[[ListList[[EE], ], AA]] ValidatedValidated[[ListList[[EE], ], AA]] ValidatedValidated[[ListList[[EE], ], AA]]    ValidatedValidated[[ListList[[EE], ], AA]]
  54. 54. ValidatedValidated[[ListList[_], _][_], _] import cats.data._ def parseAge(s: String): Validated[List[UnreadableAge], Int] def parseName(s: String): Validated[List[UnreadableName], String] def parseRate(s: String): Validated[List[UnreadableRate], Double]
  55. 55. ACCUMULATIONACCUMULATION def combine( v1: Validated[List[ParsingErr], Int], v2: Validated[List[ParsingErr], String]): Validated[List[ParsingError], (Int, String)] = (v1, v2) match { case (Valid(n), Valid(s)) => Valid((n, s)) case (Invalid(err), Valid(s)) => Invalid(err) case (Valid(n), Invalid(err)) => Invalid(err) case (Invalid(e1), Invalid(e2)) => Invalid(e1 ::: e2) }
  56. 56. WITH SEMIGROUPWITH SEMIGROUP def combine[F : Semigroup]( v1: Validated[F[ParsingErr], Int], v2: Validated[F[ParsingErr], String]): Validated[F[ParsingError], (Int, String)] = (v1, v2) match { case (Valid(n), Valid(s)) => Valid((n, s)) case (Invalid(err), Valid(s)) => Invalid(err) case (Valid(n), Invalid(err)) => Invalid(err) // powered by Semigroup[F] case (Invalid(e1), Invalid(e2)) => Invalid(e1 |+| e2) }
  57. 57. ValidatedValidated[[EE, , AA]]
  58. 58. ValidatedValidated[[FF[[EE], ], AA]] FF :  : SemigroupSemigroup
  59. 59. TUPLE SYNTAXTUPLE SYNTAX import cats._ import cats.data._ import cats.implicits._ val res: Validated[List[ParsingErr], Citizen] = ( parseAge(rec.age), parseName(rec.name), parseRate(rec.rate) ) .mapN(Citizen)
  60. 60. USINGUSING .fold().fold() val res: Validated[List[ParsingErr], Citizen] = ??? def httpErrors(errs: List[ParsingError]): HttpClientError def httpSuccess(c: Citizen): HttpSucccess val httpResponse: HttpResponse = res .fold(httpErrors, httpSuccess)
  61. 61. USINGUSING .leftMap().leftMap() val res: Validated[List[ParsingErr], Citizen] = ??? def httpErrors(errs: List[ParsingError]): HttpClientError def httpSuccess(c: Citizen): HttpSucccess val httpResponse: HttpResponse = res // Validated[List[ParsingErr], Citizen] .map(httpSuccess) // Validated[List[ParsingErr], HttpSucccess] .leftMap(httpErrors) // Validated[HttpClientError, HttpSucccess] .merge // HttpResponse
  62. 62. FIRST ERROR WINSFIRST ERROR WINS
  63. 63. ACCUMULATEDACCUMULATED ERRORSERRORS
  64. 64. FIRST ERROR WINSFIRST ERROR WINS
  65. 65. FIRST ERROR WINSFIRST ERROR WINS Modeling dependent validations Unwrapping envelopes or decoding
  66. 66. FIRST ERROR WINSFIRST ERROR WINS Modeling dependent validations Unwrapping envelopes or decoding Introducing gates/dependency onto independent validations Save on expensive computation
  67. 67. ACCUMULATED ERRORSACCUMULATED ERRORS
  68. 68. ACCUMULATED ERRORSACCUMULATED ERRORS Modeling independent validations Field-based structures (maps, records, objects)
  69. 69. ACCUMULATED ERRORSACCUMULATED ERRORS Modeling independent validations Field-based structures (maps, records, objects) Minimize round-trips
  70. 70. ACCUMULATED ERRORSACCUMULATED ERRORS Modeling independent validations Field-based structures (maps, records, objects) Minimize round-trips Embracing parallelization
  71. 71. ListList[_][_] import cats._ import cats.data._ import cats.implicits._ def parseAge(s: String): Validated[List[UnreadableAge], Int] def parseName(s: String): Validated[List[UnreadableName], String] def parseRate(s: String): Validated[List[UnreadableRate], Double] implicitly[Semigroup[List]]
  72. 72. NonEmptyListNonEmptyList[_][_] import cats._ import cats.data._ import cats.implicits._ def parseAge(s: String): Validated[NonEmptyList[UnreadableAge], Int] def parseName(s: String): Validated[NonEmptyList[UnreadableName], String] def parseRate(s: String): Validated[NonEmptyList[UnreadableRate], Double] implicitly[Semigroup[NonEmptyList]]
  73. 73. POSTFIX SYNTAXPOSTFIX SYNTAX import cats.data._ import cats.implicits._ def luckyNumber: Validated[NonEmptyList[String], Int] = if (Random.nextBoolean) 42.validNel else "You are unlucky.".invalidNel
  74. 74. APPENDINGAPPENDING List(1, 2, 3) ++ List(4)
  75. 75. APPENDING FREQUENTLYAPPENDING FREQUENTLY List(1, 2, 3) ++ List(4) List(1, 2, 3, 4) ++ List(5) List(1, 2, 3, 4, 5) ++ List(8, 9) List(1, 2) ++ List(3, 4) ++ List(8, 9)
  76. 76. LIST ACCUMULATIONLIST ACCUMULATION // read from left to right List(1, 2, 3, 4, 5) ++ List(8, 9)
  77. 77. LIST ACCUMULATION (A)LIST ACCUMULATION (A) // via repeated traversals List(1, 2, 3, 4, 5) ++ List(8, 9) List(1, 2, 3, 4) ++ List(5, 8, 9) List(1, 2, 3) ++ List(4, 5, 8, 9) List(1, 2) ++ List(3, 4, 5, 8, 9) List(1) ++ List(2, 3, 4, 5, 8, 9) List(1, 2, 3, 4, 5, 8, 9)
  78. 78. LIST ACCUMULATION (B)LIST ACCUMULATION (B) // via reverse list, built using fast prepend List(1, 2, 3, 4) ++ ReverseList() List(2, 3, 4) ++ ReverseList(1) List(3, 4) ++ ReverseList(2, 1) List(4) ++ ReverseList(3, 2, 1) List() ++ ReverseList(4, 3, 2, 1)
  79. 79. SEQUENCESSEQUENCES List(1, 2, 3, 4) Seq(5) Vector(6, 7, 8) Nested( List(9), Array(10, 11, 12))
  80. 80. CHAINCHAINED TOGETHERED TOGETHER Chain( List(1, 2, 3, 4), Seq(5), Vector(6, 7, 8), Chain( List(9), Array(10, 11, 12)))
  81. 81. ChainChain[_][_] def parseAge(s: String): Validated[Chain[UnreadableAge], Int] def parseName(s: String): Validated[Chain[UnreadableName], String] def parseRate(s: String): Validated[Chain[UnreadableRate], Double] implicitly[Semigroup[Chain]]
  82. 82. NonEmptyChainNonEmptyChain[_][_] def parseAge(s: String): Validated[NonEmptyChain[UnreadableAge], Int] def parseName(s: String): Validated[NonEmptyChain[UnreadableName], String] def parseRate(s: String): Validated[NonEmptyChain[UnreadableRate], Double] implicitly[Semigroup[NonEmptyChain]]
  83. 83. SEMIGROUPSEMIGROUP COLLECTIONSCOLLECTIONS Name Provider Empty? Accumulation performance List Scala ✅ O(n^2) NonEmptyList ❌ O(n^2) Chain ✅ constant NonEmptyChain ❌ constant
  84. 84. POSTFIX SYNTAXPOSTFIX SYNTAX import cats.implicits._ def luckyNumber: Validated[NonEmptyChain[String], Int] = if (Random.nextBoolean) 42.validNec else "You are unlucky.".invalidNec
  85. 85. ALGEBRASALGEBRAS trait ParserAlg[F[_]] { def parseAge(s: String): F[Int] def parseName(s: String): F[String] def parseRate(s: String): F[Double] }
  86. 86. TYPECLASS METHODSTYPECLASS METHODS class BadParser[F[_]](implicit F: ApplicativeError[F, Throwable]) { def parseAge(s: String): F[Int] = // raised, not thrown F.raiseError(new Exception("what a world")) }
  87. 87. NESTEDNESTED FF ANDAND GG trait FancyParserAlg[F[_], G[_]] { def parseAge(s: String): F[G[Int]] def parseName(s: String): F[G[String]] def parseRate(s: String): F[G[Double]] }
  88. 88. FF WITH TRANSFORMERWITH TRANSFORMER GTGT trait FancyParserAlg[F[_]] { def parseAge(s: String): GT[F, Int] def parseName(s: String): GT[F, String] def parseRate(s: String): GT[F, Double] }
  89. 89. FURTHER READINGFURTHER READING Contains Chain and Validated, from the Cats microsite Cats Data Types
  90. 90. JOBS.DISNEYCAREERS.COMJOBS.DISNEYCAREERS.COM
  91. 91. @@mark canlas nycmark canlas nyc

×