• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
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

on

  • 2,530 views

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 ...

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.

Statistics

Views

Total Views
2,530
Views on SlideShare
2,529
Embed Views
1

Actions

Likes
1
Downloads
9
Comments
0

1 Embed 1

https://twitter.com 1

Accessibility

Upload Details

Uploaded via as Adobe PDF

Usage Rights

CC Attribution License

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

    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 Presentation Transcript

    • 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 basic syntax assumed4. Real world use cases
    • 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")}
    • 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")}
    • 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
    • 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]
    • 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
    • OPTION IS MANDARORY!
    • 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)
    • 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}
    • OPTIONPATTERN MATCHINGDont do this (theres a better way)val foo = request.param("foo") match {case Some(value) => valuecase None => "Default foo" // Default value}
    • 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")
    • 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
    • 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}
    • 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 )
    • 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]
    • 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]
    • 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"
    • 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
    • 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...}
    • 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)}
    • 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 Either[L, R]case class Right[+L, +R](b: R) extends Either[L, R]
    • 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
    • EITHER IS NOT BIASED
    • 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")}}
    • 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}
    • 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}}
    • 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))}
    • 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)}}
    • 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]
    • 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
    • for {name <- parseName(input).rightbigName <- Right(name.capitalize).right} yield bigName
    • 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)))
    • 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]}
    • 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")}}
    • 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()}
    • 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()}
    • 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
    • SHOULD I THROW AN EXCEPTION?No, there is better approach
    • 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]
    • 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
    • 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]}
    • 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
    • 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} // Returns Catch[Int]
    • 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]
    • 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, ThreadDeath,InterruptedException, LinkageError,ControlThrowable, NotImplementedErrornonFatalCatch {println(input.toInt)}
    • 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 {println("Age parsed somehow")}.apply {input.toInt}ultimately(println("Age parsed somehow")) {input.toInt}
    • SCALA.UTIL.CONTROL._Theres more to cover and explore,please check out the .Scala documentation
    • THANKS FOR YOUR ATTENTION