– An Introduction to Options and Eithers



– An Introduction to Options and Eithers

0 0


ia-scala-options-either


On Github guesswho311 / ia-scala-options-either

An Introduction to Options and Eithers

Jonathan Quist - jonathan.quist@banno.com iascala - March 2014

Null Pointer Exception

If you have programmed in the past, chances are you've seen this guy before...

Dang Exceptions

A Null Pointer Exception occurs when you attempt to perform some operation on a null value. In scala there is also a NoSuchElementException which occurs if you try to access data that isn't there.

Heres an example:

class Apples
case object RedApple extends Apples
case object GreenApple extends Apples

def removeGreenApples(basketOfApples:List[Apples]): List[Apples] =
  basketOfApples.filterNot(apple => apple == GreenApple)

def eatAnApple(basketOfApples:List[Apples]) =
  println(basketOfApples.head)

val basketOfApples = List(GreenApple, GreenApple, RedApple, GreenApple, GreenApple)
eatAnApple(removeGreenApples(basketOfApples))
RedApple
What happens when our basketOfApples has no red apples in it?

Heres Another:

case class UserAccount(userId: Int,
                       lastName: String,
                       firstName: String,
                       age: Int)

val users:List[UserAccount] = List(UserAccount(1, "Doe", "John", 18),
                                   UserAccount(2, "Johnson", "Steve", 22)
//How can we grab a specific user from this list?
//What if the user we're looking for doesnt exist?

How can I avoid this?

  • Scala tries to solve the problem by tossing out null all together
  • Instead it provides its own type for optional values(may or may not be present)
  • The solution lies in the Option[A] trait

What is an Option?

  • Something or
  • Nothing

Option[A]

Option[A] is simply a container for the type A. If the value of type A exists then Option[A] would be an instance of Some[A]. Otherwise is there is no value then Option[A] would be None. Lets look at some code!

val hello: Option[String] = Some("Hello World!")
val pleaseNoHelloWorld: Option[String] = None
case class UserAccount(userId: Int,
                         lastName: String,
                         firstName: String,
                         age: Option[Int])

def getUserById(users: List[UserAccount], userId: Int): Option[UserAccount] = {
  users.find(user => user.userId == userId)
}

val users:List[UserAccount] = List(UserAccount(1, "Doe", "John", Some(32)),
                                   UserAccount(2, "Johnson", "Steve", Some(30)),
                                   UserAccount(3, "Jon", "Doey", None))
val user1 = getUserById(users, 1)
println(user1)
which will print out
Some(UserAccount(1,Doe,John,Some(32)))
None //if we change the call to getUserById(users, 4)
Dealing with Options
def displayAge(user: UserAccount) = user.age match {
  case Some(e) => println(e)
  case None    => println("No age provided")
}
val user1 = UserAccount(1, "Doe", "John", Some(35))
val user2 = UserAccount(2, "Smith", "Zach", None)
displayAge(user1)
displayAge(user2)
35
None
Thats cool, but pattern matching kind of seems like overkill...

Enter default values

getOrElse[B >: A](default: => B): B

  • If Some[A] exists get that, otherwise return the result of evaluating default
  • If user1.age has Something get that
  • If user1.age has Nothing get the String "No age provided"

    println(user1.age.getOrElse("No age provided"))

More Cool Stuff

  • You can map over an option
  • You can filter an option
  • You can perform side effects when an Option value is present

Mapping over an option

val user1 = Some(UserAccount(1, "Doe", "John", Some(35)))
println(user1.map(_.age))
Some(Some(35))
-- Our Results --
val user1 = Some(UserAccount(1, "Doe", "John", Some(35)))
println(user1.flatMap(_.age))
Some(35)

Filtering!

val users:List[UserAccount] = List(UserAccount(1, "Doe", "John", Some(32)),
                                   UserAccount(2, "Johnson", "Steve", Some(30)),
                                   UserAccount(3, "Smith", "Tori", None))

println(getUserById(users,1).filter(_.age.isDefined))
println(getUserById(users,2).filter(_.age.isDefined))
-- Our Results --
Some(UserAccount(1,Doe,John,Some(32)))
None

Side Effects(if you need 'em)

val user1 = Some(UserAccount(1, "Doe", "John", Some(35)))
user1.foreach(e => println(e.age))
-- Our Results --
Some(35)
object AppleSelector {
  class Apples
  case object RedApple extends Apples
  case object GreenApple extends Apples

  def removeGreenApples(basketOfApples:List[Apples]): List[Apples] =
    basketOfApples.filterNot(apple => apple == GreenApple)

  def selectAnApple(basketOfApples:List[Apples]):Option[Apples] = basketOfApples.headOption

  def eatAnApple(apple: Option[Apples]) =
    apple.fold(println("There are no edible apples"))(println(_))

  val basketOfApples = List(GreenApple, GreenApple, GreenApple, GreenApple, GreenApple, GreenApple)
  val basketOfEdibleApples = removeGreenApples(basketOfApples)
  val appleToEat = selectAnApple(basketOfEdibleApples)

  eatAnApple(appleToEat)
}

Eithers

When you get an unexpected data type You Either[Handle It, Cry yourself to sleep]

Either[+A, +B]

Eithers hold one of two possible results

Left[+A] Right[+B]

Basics

  • Like Option, Either is a container type
  • It takes two parameter types
  • Either[A,B] can contain either an instance of A or B

Making a method return an Either is Easy

sealed trait ActionsAgainstUser
case object MuteUser extends ActionsAgainstUser

def preparePost(message: String):Either[String, ActionsAgainstUser] = {
  if (message.contains("damn")) Right(WarnUser)
  else Left(message)
}

preparePost will return one of two things

  • Left[String]
  • Right[ActionsAgainstUser]

Working with Either

val postAction = preparePost("How are you doing?")
postAction match {
  case Left(msg) => println(msg)
  case Right(MuteUser) => muteUser
}

Error Catching

Originally Either was used for error handling. An Either used for error handling would look like this: Either[Throwable, Result].

However since scala 2.10 try/catch is now recommended for handling errors.

object FreedomOfSpeechCensor extends App {
  def warnUser() = println("This is your warning")
  def muteUser() = println("No more speaking out!")
  def banUser() =  println("Ban Hammer!")
  def imprisonUser() = println("Go Directly to Jail!")

  sealed trait ActionsAgainstUser
  case object WarnUser extends ActionsAgainstUser
  case object MuteUser extends ActionsAgainstUser
  case object BanUser extends ActionsAgainstUser
  case object ImprisonUser extends ActionsAgainstUser

  def determinePunishment(crime: ActionsAgainstUser) = crime match {
    case WarnUser => warnUser
    case MuteUser => muteUser
    case BanUser => banUser
    case ImprisonUser => imprisonUser
  }

  def checkMessage(message: String):Either[String, ActionsAgainstUser] = message match {
    case msg if msg.contains("damn") => Right(WarnUser)
    case msg if msg.contains("my opinion") => Right(MuteUser)
    case msg if msg.contains("I disagree with you") => Right(BanUser)
    case msg if msg.contains("Han shot second") => Right(ImprisonUser)
    case msg => Left(msg)
  }

  val message = checkMessage("damn")
  message.fold(println(msg), determinePunishment(_))
}

Banno is Hiring Scala Developers!*

wes.iliff@banno.com zach.cox@banno.com We train Scala developers too! *We're now ProfitStars