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

Post on 03-Sep-2014

2.765 views 0 download

Tags:

description

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.

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