Option, Either, Try

and what to do with corner cases when they arise

Know Your Library mini-series

By Michal Bigos / @teliatko

 

## Know Your Library - mini series 1. Hands-on with types from Scala library 2. DO's and DON'Ts 3. Intended for rookies, but Scala basic syntax assumed 4. Real world use cases
## Why null isn't an Option #### Consider following code ```java String 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 isn't an Option #### What's wrong with null ```java /* 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-existence #### Different approaches compared * Java relies on sad `null` * Groovy provides null-safe operator for accessing properties ```groovy foo?.bar?.baz ``` * Clojure uses `nil` which is okay very often, but sometimes it leads to an exception higher in call hierarchy
## Getting rid of null ### Non-existence Scala way ```scala sealed abstract class Option[A] case class Some[+A](x: A) extends Option[A] case object None extends Option[Nothing] ``` Container with one or none element
## Option 1. States that value may or may not be present on __type level__ 2. You are __forced by the compiler__ to deal with it 3. No way to __accidentally rely__ on presence of a value 4. __Clearly documents__ an intention
## Option is mandarory!
## Option #### Creating an Option ```scala val certain = Some("Sun comes up") val pitty = None ``` Never do this ```scala val nonSense = Some(null) ``` Rather use factory method on companion object ```scala val muchBetter = Option(null) // Results to None val certainAgain = Option("Sun comes up") // Some(Sun comes up) ```
## Option #### Working with Option an old way ```scala // Assume that def 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 } ``` Don't do this (only in exceptional cases)
## Option #### Pattern matching ```scala val foo = request.param("foo") match { case Some(value) => value case None => "Default foo" // Default value } ``` Don't do this (there's a better way)
## Option #### Providing a default value ```scala // any long computation for default value val foo = request.param("foo") getOrElse ("Default foo") ``` Default value is __by-name__ parameter. It's evaluated lazily.
## Option #### Treating it functional way Think of Option as collection It is __biased__ towards `Some` You can `map`, `flatMap` or compose Option(s) when it contains value, i.e. it's `Some`
## Option #### Example Suppose following model and DAO ```scala case class User(id: Int, name: String, age: Option[Int]) // In domain model, any optional value has to be expressed with Option object UserDao { def findById(id: Int): Option[User] = ... // Id can always be incorrect, e.g. it's possible that user does not exist already } ```
## Option #### Side-effecting Use case: Printing the user name ```scala // Suppose we have an userId from somewhere val userOpt = UserDao.findById(userId) // Just print user name userOpt.foreach { user => println(user.name) // Nothing will be printed when None } // Result is Unit (like void in Java) // Or more concise userOpt.foreach( user => println(user) ) // Or even more userOpt.foreach( println(_) ) userOpt.foreach( println ) ```
## Option #### map, flatMap & co. Use case: Extracting age ```scala // Extracting age val 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 2 val ageOpt = UserDao.findById(userId).flatMap( _.age.map( age => age ) ) // Returns Option[Int] ```
## Option #### For comprehensions Same use case as before ```scala // Extracting age, take 3 val ageOpt = for { user <- UserDao.findById(userId) age <- user.age } yield age // Returns Option[Int] ``` Usage in left side of generator ```scala // Extracting age, take 3 val ageOpt = for { User(_, Some(age)) <- UserDao.findById(userId) } yield age // Returns Option[Int] ```
## Option #### Composing to List Use case: Pretty-print of user ```scala def prettyPrint(user: User) = List(Option(user.name), user.age).mkString(", ") ``` Different notation ```scala def prettyPrint(user: User) = (Option(user.name) ++ user.age).mkString(", ") ``` Both prints ```scala val foo = User("Foo", Some(10)) val bar = User("Bar", None) prettyPrint(foo) // Prints "Foo, 10" prettyPrint(bar) // Prints "Bar" ``` Rule of thumb: wrap all mandatory fields with Option and then concatenate with optional ones
## Option #### Chaining Use case: Fetching or creating the user ```scala object UserDao { // New method def createUser: User } val userOpt = UserDao.findById(userId) orElse Some(UserDao.create) ``` More appropriate, when `User` is desired directly ```scala val user = UserDao.findById(userId) getOrElse UserDao.create ```
## Option #### More to explore ```scala sealed abstract class Option[A] { def fold[B](ifEmpty: ⇒ B)(f: (A) ⇒ B): B def filter(p: (A) ⇒ Boolean): Option[A] def exists(p: (A) ⇒ Boolean): Boolean ... } ```
## Is Option appropriate? Consider following piece of code ```scala case class UserFilter(name: String, age: Int) def parseFilter(input: String): Option[UserFilter] = { for { name <- parseName(input) age <- parseAge(input) } yield UserFilter(name, age) } ``` When something went wrong, cause is lost forever ```scala // Suppose that parseName and parseAge throws FilterException def parseFilter(input: String): Option[UserFilter] throws FilterException { ... } // caller side val filter = try { parseFilter(input) } catch { case e: FilterException => whatToDoInTheMiddleOfTheCode(e) } ``` Exception doesn't help much. It only introduces overhead
## Introducing Either ```scala 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] ``` Container with disjoint types.
## Either 1. States that value is either `Left[L]` or `Right[R]`, but never 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: ```scala def doSomething(): Int throws SomeException // what is this saying? two possible outcomes def doSomething(): Either[SomeException, Int] // more functional only one return value ``` 4. Again, it __clearly documents__ an intention
## Either is not biased
## Either #### Creating Either ```scala def parseAge(input: String): Either[String, Int] = { try { Right(input.toInt) } catch { case nfe: NumberFormatException => Left("Unable to parse age") } } ``` There is no `Either(...)` factory method on companion object.
## Either #### Working an old way again ```scala 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 } ``` Don't do this (only in exceptional cases)
## Either #### Pattern matching ```scala 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 } } ``` Don't do this (there's a better way)
## Either #### Projections You cannot directly use instance of `Either` as collection. It's unbiased, you have to define what is your prefered side. ```scala def parseFilter(input: String): Either[String, UserFilter] = { for { name <- parseName(input).right age <- parseAge(input).right } yield Right(UserFilter(name, age)) } ``` Working on success, only 1st error is returned. `either.right` returns `RightProjection`
## Either #### Projections, take 2 ```scala def parseFilter(input: String): Either[List[String], UserFilter] = { val name = parseName(input) val age = parseAge(input) val errors = name.left.toOption ++ age.left.toOption if (errors.isEmpty) { Right(UserFilter(name.right.get, age.right.get)) } else { Left(errors) } } ``` Working on both sides, all errors are collected. `either.left` returns `LeftProjection`
## Either #### Projections, take 3 Both projection are biased wrappers for `Either` You can use `map`, `flatMap` on them too, but beware ```scala val rightThing = Right(User("Foo", Some(10))) val projection = rightThing.right // Type is RightProjection[User] val rightThingAgain = projection.map ( _.name ) // Isn't RightProjection[User] but Right[User] ``` This is inconsistent in regdard to other collections.
## Either #### Projections, take 4 It can lead to problems with for comprehensions. ```scala for { name <- parseName(input).right bigName <- name.capitalize } yield bigName ``` This won't compile. After removing syntactic suggar, we get ```scala parseName(input).right.map { name => val bigName = name.capitalize (bigName) }.map { case (x) => x } // Map is not member of Either ``` We need projection again ```scala for { name <- parseName(input).right bigName <- Right(name.capitalize).right } yield bigName ```
## Either #### Folding Allows transforming the `Either` regardless if it's `Right` or `Left` on the same type ```scala // Once upon a time in controller parseFilter(input).fold( // Bad (Left) side transformation to HttpResponse errors => BadRequest("Error in filter") // Good (Right) side transformation to HttpResponse filter => Ok(doSomethingWith(filter)) ) ``` Accepts functions, both are evaluated lazily. Result from both functions has same type.
## Either #### More to explore ```scala sealed 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 exceptions ### Sometimes things really go wrong ```scala def parseAge(input: String): Either[String, Int] = { try { Right(input.toInt) } catch { case nfe: NumberFormatException => Left("Unable to parse age") } } ``` You can use classic `try/catch/finally` construct
## Throwing and catching exceptions ### Sometimes things really go wrong, take 2 But, it's `try/catch/finally` on steroids thanks to pattern matching ```scala try { someHorribleCodeHere() } catch { // Catching multiple types case e @ (_: IOException | _: NastyExpception) => cleanUpMess() // Catching exceptions by message case e : AnotherNastyException if e.getMessage contains "Wrong again" => cleanUpMess() // Catching all exceptions case e: Exception => cleanUpMess() } ```
## Throwing and catching exceptions ### Sometimes things really go wrong, take 3 It's powerful, but beware ```scala try { someHorribleCodeHere() } catch { // This will match scala.util.control.ControlThrowable too case _ => cleanUpMess() } ``` Never do this! ```scala try { someHorribleCodeHere() } catch { // This will match scala.util.control.ControlThrowable too case t: ControlThrowable => throw t case _ => cleanUpMess() } ``` Prefered approach of catching all
## What's wrong with exceptions 1. Referential transparency - is there a value the RHS can be replaced with? No. ```scala val something = throw new IllegalArgumentException("Foo is missing") // Result type is Nothing ``` 2. Code base can become ugly 3. Exceptions do not go well with concurrency
## Should I throw an exception? No, there is better approach
## Exception handling functional way Please welcome `import scala.util.control._` and ```scala sealed trait Try[A] case class Failure[A](e: Throwable) extends Try[A] case class Success[A](value: A) extends Try[A] ``` Collection of `Throwable` or value
## Try 1. States that computation may be `Success[T]` or may be `Failure[T]` ending with `Throwable` on type level 2. Similar to `Option`, it's `Success` biased 3. It's `try/catch` without boilerplate 4. Again it clearly documents what is happening
## Try #### Like Option All the operations from `Option` are present ```scala sealed abstract class Try[+T] { // Throws exception of Failure or return value of Success def get: T // Old way checks def isFailure: Boolean def isSuccess: Boolean // map, flatMap & Co. def map[U](f: (T) ⇒ U): Try[U] def flatMap[U](f: (T) ⇒ Try[U]): Try[U] // Side effecting def foreach[U](f: (T) ⇒ U): Unit // Default value def getOrElse[U >: T](default: ⇒ U): U // Chaining def orElse[U >: T](default: ⇒ Try[U]): Try[U] } ```
## Try #### But there is more Assume that ```scala def parseAge(input: String): Try[Int] = Try ( input.toInt ) ``` Recovering from a Failure ```scala val age = parseAge("not a number") recover { case e: NumberFormatException => 0 // Default value case _ => -1 // Another default value } // Result is always Success ``` Converting to `Option` ```scala val ageOpt = age.toOption // Will be Some if Success, None if Failure ```
## scala.util.control._ 1. Utility methods for common exception handling patterns 2. Less boiler plate than `try/catch/finally`
## scala.util.control._ #### Catching an exception ```scala catching(classOf[NumberFormatException]) { input.toInt } // Returns Catch[Int] ``` It returns `Catch[T]`
## scala.util.control._ #### Converting Converting to `Option ```scala catching(classOf[NumberFormatException]).opt { input.toInt } // Returns Option[Int] ``` ```scala failing(classOf[NumberFormatException]) { input.toInt } // Returns Option[Int] ``` Converting to `Either` ```scala catching(classOf[NumberFormatException]).either { input.toInt } // Returns Either[Throwable, Int] ``` Converting to `Try` ```scala catching(classOf[NumberFormatException]).withTry { input.toInt } // Returns Try[Int] ```
## scala.util.control._ #### Side-effecting ```scala ignoring(classOf[NumberFormatException]) { println(input.toInt) } // Returns Catch[Unit] ```
## scala.util.control._ #### Catching non-fatal exceptions ```scala nonFatalCatch { println(input.toInt) } ``` What are non-fatal exceptions? All instead of: `VirtualMachineError`, `ThreadDeath`, `InterruptedException`, `LinkageError`, `ControlThrowable`, `NotImplementedError`
## scala.util.control._ #### Providing default value ```scala val age = failAsValue(classOf[NumberFormatException])(0) { input.toInt } ```
## scala.util.control._ #### What about finally With catch logic ```scala catching(classOf[NumberFormatException]).andFinally { println("Age parsed somehow") }.apply { input.toInt } ``` No catch logic ```scala ultimately(println("Age parsed somehow")) { input.toInt } ```
## scala.util.control._ There's more to cover and explore, please check out the [Scala documentation](http://docs.scala-lang.org).

Thanks for your attention