OPTION, EITHER, TRYAND WHAT TO DO WITH CORNER CASES WHENTHEY ARISEKNOW YOUR LIBRARY MINI-SERIESBy /Michal Bigos @teliatko
KNOW YOUR LIBRARY - MINI SERIES1. Hands-on with types from Scala library2. DOs and DONTs3. Intended for rookies, but Scala...
WHY NULL ISNT AN OPTIONCONSIDER FOLLOWING CODEString foo = request.params("foo")if (foo != null) {String bar = request.par...
WHY NULL ISNT AN OPTIONWHATS WRONG WITH NULL/* 1. Nobody knows about null, not even compiler */String foo = request.params...
DEALING WITH NON-EXISTENCEDIFFERENT APPROACHES COMPAREDJava relies on sad nullGroovy provides null-safe operator for acces...
GETTING RID OF NULLNON-EXISTENCE SCALA WAYContainer with one or none elementsealed abstract class Option[A]case class Some...
OPTION1. States that value may or may not be present on type level2. You are forced by the compiler to deal with it3. No w...
OPTION IS MANDARORY!
OPTIONCREATING AN OPTIONNever do thisRather use factory method on companion objectval certain = Some("Sun comes up")val pi...
OPTIONWORKING WITH OPTION AN OLD WAYDont do this (only in exceptional cases)// Assume thatdef param[String](name: String):...
OPTIONPATTERN MATCHINGDont do this (theres a better way)val foo = request.param("foo") match {case Some(value) => valuecas...
OPTIONPROVIDING A DEFAULT VALUEDefault value is by-name parameter. Its evaluated lazily.// any long computation for defaul...
OPTIONTREATING IT FUNCTIONAL WAYThink of Option as collectionIt is biased towards SomeYou can map, flatMapor compose Optio...
OPTIONEXAMPLESuppose following model and DAOcase class User(id: Int, name: String, age: Option[Int])// In domain model, an...
OPTIONSIDE-EFFECTINGUse case: Printing the user name// Suppose we have an userId from somewhereval userOpt = UserDao.findB...
OPTIONMAP, FLATMAP & CO.Use case: Extracting age// Extracting ageval ageOpt = UserDao.findById(userId).map( _.age )// Retu...
OPTIONFOR COMPREHENSIONSSame use case as beforeUsage in left side of generator// Extracting age, take 3val ageOpt = for {u...
OPTIONCOMPOSING TO LISTUse case: Pretty-print of userDifferent notationBoth printsRule of thumb: wrap all mandatory fields...
OPTIONCHAININGUse case: Fetching or creating the userMore appropriate, when Useris desired directlyobject UserDao {// New ...
OPTIONMORE TO EXPLOREsealed abstract class Option[A] {def fold[B](ifEmpty: Ó B)(f: (A) Ó B): Bdef filter(p: (A) Ó Boolean)...
IS OPTION APPROPRIATE?Consider following piece of codeWhen something went wrong, cause is lost forevercase class UserFilte...
Exception doesnt help much. It only introduces overhead
INTRODUCING EITHERContainer with disjoint types.sealed abstract class Either[+L, +R]case class Left[+L, +R](a: L) extends ...
EITHER1. States that value is either Left[L]or Right[R], butnever both.2. No explicit sematics, but by convention Left[L]r...
EITHER IS NOT BIASED
EITHERCREATING EITHERThere is no Either(...)factory method on companionobject.def parseAge(input: String): Either[String, ...
EITHERWORKING AN OLD WAY AGAINDont do this (only in exceptional cases)def parseFilter(input: String): Either[String, Exten...
EITHERPATTERN MATCHINGDont do this (theres a better way)def parseFilter(input: String): Either[String, ExtendedFilter] = {...
EITHERPROJECTIONSYou cannot directly use instance of Eitheras collection.Its unbiased, you have to define what is your pre...
EITHERPROJECTIONS, TAKE 2Working on both sides, all errors are collected.either.leftreturns LeftProjectiondef parseFilter(...
EITHERPROJECTIONS, TAKE 3Both projection are biased wrappers for EitherYou can use map, flatMapon them too, but bewareThis...
EITHERPROJECTIONS, TAKE 4It can lead to problems with for comprehensions.This wont compile.After removing syntactic suggar...
for {name <- parseName(input).rightbigName <- Right(name.capitalize).right} yield bigName
EITHERFOLDINGAllows transforming the Eitherregardless if its RightorLefton the same typeAccepts functions, both are evalua...
EITHERMORE TO EXPLOREsealed abstract class Either[+A, +B] {def joinLeft[A1 >: A, B1 >: B, C](implicit ev: <:<[A1, Either[C...
THROWING AND CATCHING EXCEPTIONSSOMETIMES THINGS REALLY GO WRONGYou can use classic try/catch/finallyconstructdef parseAge...
THROWING AND CATCHING EXCEPTIONSSOMETIMES THINGS REALLY GO WRONG, TAKE 2But, its try/catch/finallyon steroids thanks to pa...
THROWING AND CATCHING EXCEPTIONSSOMETIMES THINGS REALLY GO WRONG, TAKE 3Its powerful, but bewareNever do this!Prefered app...
WHATS WRONG WITH EXCEPTIONS1. Referential transparency - is there a value the RHS can bereplaced with? No.2. Code base can...
SHOULD I THROW AN EXCEPTION?No, there is better approach
EXCEPTION HANDLING FUNCTIONAL WAYPlease welcomeimport scala.util.control._andCollection of Throwableor valuesealed trait T...
TRY1. States that computation may be Success[T]or may beFailure[T]ending with Throwableon type level2. Similar to Option, ...
TRYLIKE OPTIONAll the operations from Optionare presentsealed abstract class Try[+T] {// Throws exception of Failure or re...
TRYBUT THERE IS MOREAssume thatRecovering from a FailureConverting to Optiondef parseAge(input: String): Try[Int] = Try ( ...
SCALA.UTIL.CONTROL._1. Utility methods for common exception handling patterns2. Less boiler plate than try/catch/finally
SCALA.UTIL.CONTROL._CATCHING AN EXCEPTIONIt returns Catch[T]catching(classOf[NumberFormatException]) {input.toInt} // Retu...
SCALA.UTIL.CONTROL._CONVERTINGConverting to `OptionConverting to EitherConverting to Trycatching(classOf[NumberFormatExcep...
SCALA.UTIL.CONTROL._SIDE-EFFECTINGignoring(classOf[NumberFormatException]) {println(input.toInt)} // Returns Catch[Unit]
SCALA.UTIL.CONTROL._CATCHING NON-FATAL EXCEPTIONSWhat are non-fatal exceptions?All instead of:VirtualMachineError, ThreadD...
SCALA.UTIL.CONTROL._PROVIDING DEFAULT VALUEval age = failAsValue(classOf[NumberFormatException])(0) {input.toInt}
SCALA.UTIL.CONTROL._WHAT ABOUT FINALLYWith catch logicNo catch logiccatching(classOf[NumberFormatException]).andFinally {p...
SCALA.UTIL.CONTROL._Theres more to cover and explore,please check out the .Scala documentation
THANKS FOR YOUR ATTENTION
Option, Either, Try and what to do with corner cases when they arise
Option, Either, Try and what to do with corner cases when they arise
Option, Either, Try and what to do with corner cases when they arise
Upcoming SlideShare
Loading in …5
×

Option, Either, Try and what to do with corner cases when they arise

4,738 views

Published on

Part of mini-series of talks about gems in Scala standard library. Used for education of junior developers in our company. This pare is about Option, Either, Try and error handling in Scala in general.

1 Comment
6 Likes
Statistics
Notes
No Downloads
Views
Total views
4,738
On SlideShare
0
From Embeds
0
Number of Embeds
2
Actions
Shares
0
Downloads
28
Comments
1
Likes
6
Embeds 0
No embeds

No notes for slide

Option, Either, Try and what to do with corner cases when they arise

  1. 1. OPTION, EITHER, TRYAND WHAT TO DO WITH CORNER CASES WHENTHEY ARISEKNOW YOUR LIBRARY MINI-SERIESBy /Michal Bigos @teliatko
  2. 2. KNOW YOUR LIBRARY - MINI SERIES1. Hands-on with types from Scala library2. DOs and DONTs3. Intended for rookies, but Scala basic syntax assumed4. Real world use cases
  3. 3. WHY NULL ISNT AN OPTIONCONSIDER FOLLOWING CODEString foo = request.params("foo")if (foo != null) {String bar = request.params("bar")if (bar != null) {doSomething(foo, bar)} else {throw new ApplicationException("Bar not found")}} else {throw new ApplicationException("Foo not found")}
  4. 4. WHY NULL ISNT AN OPTIONWHATS WRONG WITH NULL/* 1. Nobody knows about null, not even compiler */String foo = request.params("foo")/* 2. Annoying checking */if (foo != null) {String bar = request.params("bar")// if (bar != null) {/* 3. Danger of infamous NullPointerException,everbody can forget some check */doSomething(foo, bar)// } else {/* 4. Optionated detailed failures,sometimes failure in the end is enough */// throw new ApplicationException("Bar not found")// }} else {/* 5. Design flaw, just original exception replacement */throw new ApplicationException("Foo not found")}
  5. 5. DEALING WITH NON-EXISTENCEDIFFERENT APPROACHES COMPAREDJava relies on sad nullGroovy provides null-safe operator for accessingpropertiesClojure uses nilwhich is okay very often, but sometimesit leads to an exception higher in call hierarchyfoo?.bar?.baz
  6. 6. GETTING RID OF NULLNON-EXISTENCE SCALA WAYContainer with one or none elementsealed abstract class Option[A]case class Some[+A](x: A) extends Option[A]case object None extends Option[Nothing]
  7. 7. OPTION1. States that value may or may not be present on type level2. You are forced by the compiler to deal with it3. No way to accidentally rely on presence of a value4. Clearly documents an intention
  8. 8. OPTION IS MANDARORY!
  9. 9. OPTIONCREATING AN OPTIONNever do thisRather use factory method on companion objectval certain = Some("Sun comes up")val pitty = Noneval nonSense = Some(null)val muchBetter = Option(null) // Results to Noneval certainAgain = Option("Sun comes up") // Some(Sun comes up)
  10. 10. OPTIONWORKING WITH OPTION AN OLD WAYDont do this (only in exceptional cases)// Assume thatdef param[String](name: String): Option[String] ...val fooParam = request.param("foo")val foo = if (fooParam.isDefined) {fooParam.get // throws NoSuchElementException when None} else {"Default foo" // Default value}
  11. 11. OPTIONPATTERN MATCHINGDont do this (theres a better way)val foo = request.param("foo") match {case Some(value) => valuecase None => "Default foo" // Default value}
  12. 12. OPTIONPROVIDING A DEFAULT VALUEDefault value is by-name parameter. Its evaluated lazily.// any long computation for default valueval foo = request.param("foo") getOrElse ("Default foo")
  13. 13. OPTIONTREATING IT FUNCTIONAL WAYThink of Option as collectionIt is biased towards SomeYou can map, flatMapor compose Option(s) when itcontains value, i.e. its Some
  14. 14. OPTIONEXAMPLESuppose following model and DAOcase class User(id: Int, name: String, age: Option[Int])// In domain model, any optional value has to be expressed with Optionobject UserDao {def findById(id: Int): Option[User] = ...// Id can always be incorrect, e.g. its possible that user does notexist already}
  15. 15. OPTIONSIDE-EFFECTINGUse case: Printing the user name// Suppose we have an userId from somewhereval userOpt = UserDao.findById(userId)// Just print user nameuserOpt.foreach { user =>println(user.name) // Nothing will be printed when None} // Result is Unit (like void in Java)// Or more conciseuserOpt.foreach( user => println(user) )// Or even moreuserOpt.foreach( println(_) )userOpt.foreach( println )
  16. 16. OPTIONMAP, FLATMAP & CO.Use case: Extracting age// Extracting ageval ageOpt = UserDao.findById(userId).map( _.age )// Returns Option[Option[Int]]val ageOpt = UserDao.findById(userId).map( _.age.map( age => age ) )// ReturnsOption[Option[Int]] too// Extracting age, take 2val ageOpt = UserDao.findById(userId).flatMap( _.age.map( age => age ))// Returns Option[Int]
  17. 17. OPTIONFOR COMPREHENSIONSSame use case as beforeUsage in left side of generator// Extracting age, take 3val ageOpt = for {user <- UserDao.findById(userId)age <- user.age} yield age // Returns Option[Int]// Extracting age, take 3val ageOpt = for {User(_, Some(age)) <- UserDao.findById(userId)} yield age // Returns Option[Int]
  18. 18. OPTIONCOMPOSING TO LISTUse case: Pretty-print of userDifferent notationBoth printsRule of thumb: wrap all mandatory fields with Option andthen concatenate with optional onesdef prettyPrint(user: User) =List(Option(user.name), user.age).mkString(", ")def prettyPrint(user: User) =(Option(user.name) ++ user.age).mkString(", ")val foo = User("Foo", Some(10))val bar = User("Bar", None)prettyPrint(foo) // Prints "Foo, 10"prettyPrint(bar) // Prints "Bar"
  19. 19. OPTIONCHAININGUse case: Fetching or creating the userMore appropriate, when Useris desired directlyobject UserDao {// New methoddef createUser: User}val userOpt = UserDao.findById(userId) orElse Some(UserDao.create)val user = UserDao.findById(userId) getOrElse UserDao.create
  20. 20. OPTIONMORE TO EXPLOREsealed abstract class Option[A] {def fold[B](ifEmpty: Ó B)(f: (A) Ó B): Bdef filter(p: (A) Ó Boolean): Option[A]def exists(p: (A) Ó Boolean): Boolean...}
  21. 21. IS OPTION APPROPRIATE?Consider following piece of codeWhen something went wrong, cause is lost forevercase class UserFilter(name: String, age: Int)def parseFilter(input: String): Option[UserFilter] = {for {name <- parseName(input)age <- parseAge(input)} yield UserFilter(name, age)}// Suppose that parseName and parseAge throws FilterExceptiondef parseFilter(input: String): Option[UserFilter]throws FilterException { ... }// caller sideval filter = try {parseFilter(input)} catch {case e: FilterException => whatToDoInTheMiddleOfTheCode(e)}
  22. 22. Exception doesnt help much. It only introduces overhead
  23. 23. INTRODUCING EITHERContainer with disjoint types.sealed abstract class Either[+L, +R]case class Left[+L, +R](a: L) extends Either[L, R]case class Right[+L, +R](b: R) extends Either[L, R]
  24. 24. EITHER1. States that value is either Left[L]or Right[R], butnever both.2. No explicit sematics, but by convention Left[L]represents corner case and Right[R]desired one.3. Functional way of dealing with alternatives, consider:4. Again, it clearly documents an intentiondef doSomething(): Int throws SomeException// what is this saying? two possible outcomesdef doSomething(): Either[SomeException, Int]// more functional only one return value
  25. 25. EITHER IS NOT BIASED
  26. 26. EITHERCREATING EITHERThere is no Either(...)factory method on companionobject.def parseAge(input: String): Either[String, Int] = {try {Right(input.toInt)} catch {case nfe: NumberFormatException => Left("Unable to parse age")}}
  27. 27. EITHERWORKING AN OLD WAY AGAINDont do this (only in exceptional cases)def parseFilter(input: String): Either[String, ExtendedFilter] = {val name = parseName(input)if (name.isRight) {val age = parseAge(input)if (age.isRight) {Right(UserFilter(time, rating))} else age} else name}
  28. 28. EITHERPATTERN MATCHINGDont do this (theres a better way)def parseFilter(input: String): Either[String, ExtendedFilter] = {parseName(input) match {case Right(name) => parseAge(input) match {case Right(age) => UserFilter(name, age)case error: Left[_] => error}case error: Left[_] => error}}
  29. 29. EITHERPROJECTIONSYou cannot directly use instance of Eitheras collection.Its unbiased, you have to define what is your prefered side.Working on success, only 1st error is returned.either.rightreturns RightProjectiondef parseFilter(input: String): Either[String, UserFilter] = {for {name <- parseName(input).rightage <- parseAge(input).right} yield Right(UserFilter(name, age))}
  30. 30. EITHERPROJECTIONS, TAKE 2Working on both sides, all errors are collected.either.leftreturns LeftProjectiondef parseFilter(input: String): Either[List[String], UserFilter] = {val name = parseName(input)val age = parseAge(input)val errors = name.left.toOption ++ age.left.toOptionif (errors.isEmpty) {Right(UserFilter(name.right.get, age.right.get))} else {Left(errors)}}
  31. 31. EITHERPROJECTIONS, TAKE 3Both projection are biased wrappers for EitherYou can use map, flatMapon them too, but bewareThis is inconsistent in regdard to other collections.val rightThing = Right(User("Foo", Some(10)))val projection = rightThing.right // Type is RightProjection[User]val rightThingAgain = projection.map ( _.name )// Isnt RightProjection[User] but Right[User]
  32. 32. EITHERPROJECTIONS, TAKE 4It can lead to problems with for comprehensions.This wont compile.After removing syntactic suggar, we getWe need projection againfor {name <- parseName(input).rightbigName <- name.capitalize} yield bigNameparseName(input).right.map { name =>val bigName = name.capitalize(bigName)}.map { case (x) => x } // Map is not member of Either
  33. 33. for {name <- parseName(input).rightbigName <- Right(name.capitalize).right} yield bigName
  34. 34. EITHERFOLDINGAllows transforming the Eitherregardless if its RightorLefton the same typeAccepts functions, both are evaluated lazily. Result from bothfunctions has same type.// Once upon a time in controllerparseFilter(input).fold(// Bad (Left) side transformation to HttpResponseerrors => BadRequest("Error in filter")// Good (Right) side transformation to HttpResponsefilter => Ok(doSomethingWith(filter)))
  35. 35. EITHERMORE TO EXPLOREsealed abstract class Either[+A, +B] {def joinLeft[A1 >: A, B1 >: B, C](implicit ev: <:<[A1, Either[C, B1]]): Either[C, B1]def joinRight[A1 >: A, B1 >: B, C](implicit ev: <:<[B1, Either[A1,C]]): Either[A1, C]def swap: Product with Serializable with Either[B, A]}
  36. 36. THROWING AND CATCHING EXCEPTIONSSOMETIMES THINGS REALLY GO WRONGYou can use classic try/catch/finallyconstructdef parseAge(input: String): Either[String, Int] = {try {Right(input.toInt)} catch {case nfe: NumberFormatException => Left("Unable to parse age")}}
  37. 37. THROWING AND CATCHING EXCEPTIONSSOMETIMES THINGS REALLY GO WRONG, TAKE 2But, its try/catch/finallyon steroids thanks to patternmatchingtry {someHorribleCodeHere()} catch {// Catching multiple typescase e @ (_: IOException | _: NastyExpception) => cleanUpMess()// Catching exceptions by messagecase e : AnotherNastyExceptionif e.getMessage contains "Wrong again" => cleanUpMess()// Catching all exceptionscase e: Exception => cleanUpMess()}
  38. 38. THROWING AND CATCHING EXCEPTIONSSOMETIMES THINGS REALLY GO WRONG, TAKE 3Its powerful, but bewareNever do this!Prefered approach of catching alltry {someHorribleCodeHere()} catch {// This will match scala.util.control.ControlThrowable toocase _ => cleanUpMess()}try {someHorribleCodeHere()} catch {// This will match scala.util.control.ControlThrowable toocase t: ControlThrowable => throw tcase _ => cleanUpMess()}
  39. 39. WHATS WRONG WITH EXCEPTIONS1. Referential transparency - is there a value the RHS can bereplaced with? No.2. Code base can become ugly3. Exceptions do not go well with concurrencyval something = throw new IllegalArgumentException("Foo is missing")// Result type is Nothing
  40. 40. SHOULD I THROW AN EXCEPTION?No, there is better approach
  41. 41. EXCEPTION HANDLING FUNCTIONAL WAYPlease welcomeimport scala.util.control._andCollection of Throwableor valuesealed trait Try[A]case class Failure[A](e: Throwable) extends Try[A]case class Success[A](value: A) extends Try[A]
  42. 42. TRY1. States that computation may be Success[T]or may beFailure[T]ending with Throwableon type level2. Similar to Option, its Successbiased3. Its try/catchwithout boilerplate4. Again it clearly documents what is happening
  43. 43. TRYLIKE OPTIONAll the operations from Optionare presentsealed abstract class Try[+T] {// Throws exception of Failure or return value of Successdef get: T// Old way checksdef isFailure: Booleandef isSuccess: Boolean// map, flatMap & Co.def map[U](f: (T) Ó U): Try[U]def flatMap[U](f: (T) Ó Try[U]): Try[U]// Side effectingdef foreach[U](f: (T) Ó U): Unit// Default valuedef getOrElse[U >: T](default: Ó U): U// Chainingdef orElse[U >: T](default: Ó Try[U]): Try[U]}
  44. 44. TRYBUT THERE IS MOREAssume thatRecovering from a FailureConverting to Optiondef parseAge(input: String): Try[Int] = Try ( input.toInt )val age = parseAge("not a number") recover {case e: NumberFormatException => 0 // Default valuecase _ => -1 // Another default value} // Result is always Successval ageOpt = age.toOption// Will be Some if Success, None if Failure
  45. 45. SCALA.UTIL.CONTROL._1. Utility methods for common exception handling patterns2. Less boiler plate than try/catch/finally
  46. 46. SCALA.UTIL.CONTROL._CATCHING AN EXCEPTIONIt returns Catch[T]catching(classOf[NumberFormatException]) {input.toInt} // Returns Catch[Int]
  47. 47. SCALA.UTIL.CONTROL._CONVERTINGConverting to `OptionConverting to EitherConverting to Trycatching(classOf[NumberFormatException]).opt {input.toInt} // Returns Option[Int]failing(classOf[NumberFormatException]) {input.toInt} // Returns Option[Int]catching(classOf[NumberFormatException]).either {input.toInt} // Returns Either[Throwable, Int]catching(classOf[NumberFormatException]).withTry {input.toInt} // Returns Try[Int]
  48. 48. SCALA.UTIL.CONTROL._SIDE-EFFECTINGignoring(classOf[NumberFormatException]) {println(input.toInt)} // Returns Catch[Unit]
  49. 49. SCALA.UTIL.CONTROL._CATCHING NON-FATAL EXCEPTIONSWhat are non-fatal exceptions?All instead of:VirtualMachineError, ThreadDeath,InterruptedException, LinkageError,ControlThrowable, NotImplementedErrornonFatalCatch {println(input.toInt)}
  50. 50. SCALA.UTIL.CONTROL._PROVIDING DEFAULT VALUEval age = failAsValue(classOf[NumberFormatException])(0) {input.toInt}
  51. 51. SCALA.UTIL.CONTROL._WHAT ABOUT FINALLYWith catch logicNo catch logiccatching(classOf[NumberFormatException]).andFinally {println("Age parsed somehow")}.apply {input.toInt}ultimately(println("Age parsed somehow")) {input.toInt}
  52. 52. SCALA.UTIL.CONTROL._Theres more to cover and explore,please check out the .Scala documentation
  53. 53. THANKS FOR YOUR ATTENTION

×