Option, Either, Try and what to do with corner cases when they arise
-
Upload
michal-bigos -
Category
Education
-
view
2.765 -
download
0
description
Transcript of Option, Either, Try and what to do with corner cases when they arise
OPTION, EITHER, TRYAND WHAT TO DO WITH CORNER CASES WHEN
THEY ARISEKNOW YOUR LIBRARY MINI-SERIES
By /
Michal Bigos @teliatko
KNOW YOUR LIBRARY - MINI SERIES1. Hands-on with types from Scala library2. DO's and DON'Ts3. Intended for rookies, but Scala basic syntax assumed4. Real world use cases
WHY NULL ISN'T AN OPTIONCONSIDER FOLLOWING CODE
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 OPTIONWHAT'S 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 COMPARED
Java relies on sad nullGroovy provides null-safe operator for accessingproperties
Clojure uses nil which is okay very often, but sometimesit leads to an exception higher in call hierarchy
foo?.bar?.baz
GETTING RID OF NULLNON-EXISTENCE SCALA WAY
Container with one or none element
sealed 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 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!
OPTIONCREATING AN OPTION
Never do this
Rather use factory method on companion object
val certain = Some("Sun comes up")val pitty = None
val nonSense = Some(null)
val muchBetter = Option(null) // Results to Noneval certainAgain = Option("Sun comes up") // Some(Sun comes up)
OPTIONWORKING WITH OPTION AN OLD WAY
Don't 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 MATCHING
Don't do this (there's a better way)
val foo = request.param("foo") match { case Some(value) => value case None => "Default foo" // Default value}
OPTIONPROVIDING A DEFAULT VALUE
Default value is by-name parameter. It's evaluated lazily.
// any long computation for default valueval foo = request.param("foo") getOrElse ("Default foo")
OPTIONTREATING IT FUNCTIONAL WAY
Think of Option as collection
It is biased towards Some
You can map, flatMap or compose Option(s) when itcontains value, i.e. it's Some
OPTIONEXAMPLE
Suppose following model and DAOcase 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}
OPTIONSIDE-EFFECTING
Use 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 more userOpt.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 COMPREHENSIONS
Same use case as before
Usage 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 LIST
Use case: Pretty-print of user
Different notation
Both prints
Rule of thumb: wrap all mandatory fields with Option andthen concatenate with optional ones
def 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"
OPTIONCHAINING
Use case: Fetching or creating the user
More appropriate, when User is desired directly
object UserDao { // New method def createUser: User}
val userOpt = UserDao.findById(userId) orElse Some(UserDao.create)
val user = UserDao.findById(userId) getOrElse UserDao.create
OPTIONMORE TO EXPLORE
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
When something went wrong, cause is lost forever
case 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 doesn't help much. It only introduces overhead
INTRODUCING EITHER
Container 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], 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:
4. Again, it clearly documents an intention
def doSomething(): Int throws SomeException // what is this saying? two possible outcomes def doSomething(): Either[SomeException, Int]// more functional only one return value
EITHER IS NOT BIASED
EITHERCREATING EITHER
There 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 AGAIN
Don't 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 MATCHING
Don't do this (there's 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 }}
EITHERPROJECTIONS
You cannot directly use instance of Either as collection.
It's unbiased, you have to define what is your prefered side.
Working on success, only 1st error is returned.
either.right returns RightProjection
def parseFilter(input: String): Either[String, UserFilter] = { for { name <- parseName(input).right age <- parseAge(input).right } yield Right(UserFilter(name, age))}
EITHERPROJECTIONS, TAKE 2
Working on both sides, all errors are collected.
either.left returns LeftProjection
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) }}
EITHERPROJECTIONS, TAKE 3
Both projection are biased wrappers for Either
You can use map, flatMap on them too, but beware
This 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 ) // Isn't RightProjection[User] but Right[User]
EITHERPROJECTIONS, TAKE 4
It can lead to problems with for comprehensions.
This won't compile.
After removing syntactic suggar, we get
We need projection again
for { name <- parseName(input).right bigName <- name.capitalize} yield bigName
parseName(input).right.map { name => val bigName = name.capitalize (bigName)}.map { case (x) => x } // Map is not member of Either
for { name <- parseName(input).right bigName <- Right(name.capitalize).right} yield bigName
EITHERFOLDING
Allows transforming the Either regardless if it's Right orLeft on the same type
Accepts functions, both are evaluated lazily. Result from bothfunctions has same type.
// Once upon a time in controllerparseFilter(input).fold( // Bad (Left) side transformation to HttpResponse errors => BadRequest("Error in filter") // Good (Right) side transformation to HttpResponse filter => Ok(doSomethingWith(filter)))
EITHERMORE TO EXPLORE
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 EXCEPTIONSSOMETIMES THINGS REALLY GO WRONG
You can use classic try/catch/finally construct
def 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 2
But, it's try/catch/finally on steroids thanks to patternmatching
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 EXCEPTIONSSOMETIMES THINGS REALLY GO WRONG, TAKE 3
It's powerful, but beware
Never do this!
Prefered approach of catching all
try { someHorribleCodeHere()} catch { // This will match scala.util.control.ControlThrowable too case _ => cleanUpMess()}
try { someHorribleCodeHere()} catch { // This will match scala.util.control.ControlThrowable too case t: ControlThrowable => throw t case _ => cleanUpMess()}
WHAT'S WRONG WITH EXCEPTIONS1. Referential transparency - is there a value the RHS can be
replaced with? No.
2. Code base can become ugly
3. Exceptions do not go well with concurrency
val 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 welcome
import scala.util.control._
and
Collection of Throwable or value
sealed 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 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
TRYLIKE OPTION
All the operations from Option are presentsealed 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]}
TRYBUT THERE IS MORE
Assume that
Recovering from a Failure
Converting to Option
def parseAge(input: String): Try[Int] = Try ( input.toInt )
val age = parseAge("not a number") recover { case e: NumberFormatException => 0 // Default value case _ => -1 // Another default value} // Result is always Success
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
It returns Catch[T]
catching(classOf[NumberFormatException]) { input.toInt} // Returns Catch[Int]
SCALA.UTIL.CONTROL._CONVERTING
Converting to `Option
Converting to Either
Converting to Try
catching(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-EFFECTING
ignoring(classOf[NumberFormatException]) { println(input.toInt)} // Returns Catch[Unit]
SCALA.UTIL.CONTROL._CATCHING NON-FATAL EXCEPTIONS
What are non-fatal exceptions?
All instead of:
VirtualMachineError, ThreadDeath,InterruptedException, LinkageError,ControlThrowable, NotImplementedError
nonFatalCatch { println(input.toInt)}
SCALA.UTIL.CONTROL._PROVIDING DEFAULT VALUE
val age = failAsValue(classOf[NumberFormatException])(0) { input.toInt}
SCALA.UTIL.CONTROL._WHAT ABOUT FINALLY
With catch logic
No catch logic
catching(classOf[NumberFormatException]).andFinally { println("Age parsed somehow")}.apply { input.toInt}
ultimately(println("Age parsed somehow")) { input.toInt}
SCALA.UTIL.CONTROL._There's more to cover and explore,
please check out the .Scala documentation
THANKS FOR YOUR ATTENTION