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

56
OPTION, EITHER, TRY AND WHAT TO DO WITH CORNER CASES WHEN THEY ARISE KNOW YOUR LIBRARY MINI-SERIES By / Michal Bigos @teliatko

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

Page 1: 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

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

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

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

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")}

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

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") }

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

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

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

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]

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

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

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

OPTION IS MANDARORY!

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

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)

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

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}

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

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}

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

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")

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

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

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

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}

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

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 )

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

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]

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

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]

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

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"

Page 19: Option, Either, Try and what to do with corner cases when they arise
Page 20: Option, Either, Try and what to do with corner cases when they arise

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

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

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

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

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)}

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

Exception doesn't help much. It only introduces overhead

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

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]

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

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

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

EITHER IS NOT BIASED

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

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") }}

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

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}

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

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

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

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))}

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

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) }}

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

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]

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

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

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

for { name <- parseName(input).right bigName <- Right(name.capitalize).right} yield bigName

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

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

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

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]}

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

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") }}

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

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()}

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

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()}

Page 40: Option, Either, Try and what to do with corner cases when they arise
Page 41: Option, Either, Try and what to do with corner cases when they arise

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

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

SHOULD I THROW AN EXCEPTION?No, there is better approach

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

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]

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

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

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

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]}

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

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

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

SCALA.UTIL.CONTROL._1. Utility methods for common exception handling patterns

2. Less boiler plate than try/catch/finally

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

SCALA.UTIL.CONTROL._CATCHING AN EXCEPTION

It returns Catch[T]

catching(classOf[NumberFormatException]) { input.toInt} // Returns Catch[Int]

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

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]

Page 50: Option, Either, Try and what to do with corner cases when they arise
Page 51: Option, Either, Try and what to do with corner cases when they arise

SCALA.UTIL.CONTROL._SIDE-EFFECTING

ignoring(classOf[NumberFormatException]) { println(input.toInt)} // Returns Catch[Unit]

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

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)}

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

SCALA.UTIL.CONTROL._PROVIDING DEFAULT VALUE

val age = failAsValue(classOf[NumberFormatException])(0) { input.toInt}

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

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}

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

SCALA.UTIL.CONTROL._There's more to cover and explore,

please check out the .Scala documentation

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

THANKS FOR YOUR ATTENTION