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.
Russian Escort Service in Delhi 11k Hotel Foreigner Russian Call Girls in Delhi
Option, Either, Try and what to do with corner cases when they arise
1. OPTION, EITHER, TRY
AND WHAT TO DO WITH CORNER CASES WHEN
THEY ARISE
KNOW YOUR LIBRARY MINI-SERIES
By /Michal Bigos @teliatko
2. 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
3. WHY NULL ISN'T AN OPTION
CONSIDER 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")
}
4. WHY NULL ISN'T AN OPTION
WHAT'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")
}
5. DEALING WITH NON-EXISTENCE
DIFFERENT APPROACHES COMPARED
Java relies on sad null
Groovy provides null-safe operator for accessing
properties
Clojure uses nilwhich is okay very often, but sometimes
it leads to an exception higher in call hierarchy
foo?.bar?.baz
6. GETTING RID OF NULL
NON-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]
7. 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
9. OPTION
CREATING 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 None
val certainAgain = Option("Sun comes up") // Some(Sun comes up)
10. OPTION
WORKING WITH OPTION AN OLD WAY
Don't do this (only in exceptional cases)
// 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
}
11. OPTION
PATTERN 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
}
12. OPTION
PROVIDING A DEFAULT VALUE
Default value is by-name parameter. It's evaluated lazily.
// any long computation for default value
val foo = request.param("foo") getOrElse ("Default foo")
13. OPTION
TREATING IT FUNCTIONAL WAY
Think of Option as collection
It is biased towards Some
You can map, flatMapor compose Option(s) when it
contains value, i.e. it's Some
14. OPTION
EXAMPLE
Suppose following model and DAO
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
}
15. OPTION
SIDE-EFFECTING
Use case: Printing the user name
// 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 )
16. OPTION
MAP, FLATMAP & CO.
Use case: Extracting age
// 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]
17. OPTION
FOR COMPREHENSIONS
Same use case as before
Usage in left side of generator
// Extracting age, take 3
val ageOpt = for {
user <- UserDao.findById(userId)
age <- user.age
} yield age // Returns Option[Int]
// Extracting age, take 3
val ageOpt = for {
User(_, Some(age)) <- UserDao.findById(userId)
} yield age // Returns Option[Int]
18. OPTION
COMPOSING TO LIST
Use case: Pretty-print of user
Different notation
Both prints
Rule of thumb: wrap all mandatory fields with Option and
then 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"
19.
20. OPTION
CHAINING
Use case: Fetching or creating the user
More appropriate, when Useris 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
21. OPTION
MORE 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
...
}
22. 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 FilterException
def parseFilter(input: String): Option[UserFilter]
throws FilterException { ... }
// caller side
val filter = try {
parseFilter(input)
} catch {
case e: FilterException => whatToDoInTheMiddleOfTheCode(e)
}
24. 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]
25. 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:
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
27. EITHER
CREATING EITHER
There is no Either(...)factory method on companion
object.
def parseAge(input: String): Either[String, Int] = {
try {
Right(input.toInt)
} catch {
case nfe: NumberFormatException => Left("Unable to parse age")
}
}
28. EITHER
WORKING 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
}
29. EITHER
PATTERN 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
}
}
30. EITHER
PROJECTIONS
You cannot directly use instance of Eitheras collection.
It's unbiased, you have to define what is your prefered side.
Working on success, only 1st error is returned.
either.rightreturns RightProjection
def parseFilter(input: String): Either[String, UserFilter] = {
for {
name <- parseName(input).right
age <- parseAge(input).right
} yield Right(UserFilter(name, age))
}
31. EITHER
PROJECTIONS, TAKE 2
Working on both sides, all errors are collected.
either.leftreturns 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)
}
}
32. EITHER
PROJECTIONS, TAKE 3
Both projection are biased wrappers for Either
You can use map, flatMapon 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]
33. EITHER
PROJECTIONS, 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
34. for {
name <- parseName(input).right
bigName <- Right(name.capitalize).right
} yield bigName
35. EITHER
FOLDING
Allows transforming the Eitherregardless if it's Rightor
Lefton the same type
Accepts functions, both are evaluated lazily. Result from both
functions has same type.
// 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))
)
36. EITHER
MORE 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]
}
37. THROWING AND CATCHING EXCEPTIONS
SOMETIMES THINGS REALLY GO WRONG
You can use classic try/catch/finallyconstruct
def parseAge(input: String): Either[String, Int] = {
try {
Right(input.toInt)
} catch {
case nfe: NumberFormatException => Left("Unable to parse age")
}
}
38. THROWING AND CATCHING EXCEPTIONS
SOMETIMES THINGS REALLY GO WRONG, TAKE 2
But, it's try/catch/finallyon steroids thanks to pattern
matching
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()
}
39. THROWING AND CATCHING EXCEPTIONS
SOMETIMES 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()
}
40.
41. WHAT'S WRONG WITH EXCEPTIONS
1. 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
42. SHOULD I THROW AN EXCEPTION?
No, there is better approach
43. EXCEPTION HANDLING FUNCTIONAL WAY
Please welcome
import scala.util.control._
and
Collection of Throwableor value
sealed trait Try[A]
case class Failure[A](e: Throwable) extends Try[A]
case class Success[A](value: A) extends Try[A]
44. TRY
1. States that computation may be Success[T]or may be
Failure[T]ending with Throwableon type level
2. Similar to Option, it's Successbiased
3. It's try/catchwithout boilerplate
4. Again it clearly documents what is happening
45. TRY
LIKE OPTION
All the operations from Optionare present
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]
}
46. TRY
BUT 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