def mean(xs: Seq[Double], empty: Double): Double =
if (xs.isEmpty) empty
else xs.sum / xs.length
def mean(xs: Seq[Double]): Double =
if (xs.isEmpty) Double.NaN
else xs.sum / xs.length
def mean(xs: Seq[Double]): Double =
if (xs.isEmpty)
throw new ArithmeticException("NaN")
else xs.sum / xs.length
Exceptions are not type-safe or referentially transparent.
The type of mean
, Seq[Double]) => Double
tells us nothing about the fact that exceptions may occur, and the compiler will not force callers of mean
to make a decision about how to handle those exceptions.
The mean
function is an example of what’s called a partial function: it’s not defined for some inputs.
A function is typically partial because it makes some assumptions about its inputs that aren’t implied by the input types.
Option
Option
is a monadic container that may or may not hold something. It tells you that a function might not return what you’re asking for.
val numbers = Map("one" -> 1, "two" -> 2)
//numbers: Map(one -> 1, two -> 2)
numbers.get("two")
//res0: Option[Int] = Some(2)
numbers.get("three")
//res1: Option[Int] = None
def mean(xs: Seq[Double]): Option[Double] =
if (xs.isEmpty) None
else Some(xs.sum / xs.length)
Option
itself is generic and has two subclasses: Some[T]
or None
trait Option[+A] //base trait
case class Some[+A](get: A) extends Option[A]
case object None extends Option[Nothing]
Option
s are like List
s with at most a single element.
trait List[+A] //base trait
case class Cons[+A](head: A, tail: List[A]) extends List[A]
case object Nil extends List[Nothing]
Here is a toy implementation of Option
:
trait Option[+A] {
def unit[A](a: A): Option[A] = Some(a)
def flatMap[B](f: A => Option[B]): Option[B] =
this match {
case None => None
case Some(a) => f(a)
}
def map[B](f: A => B): Option[B] =
flatMap(f andThen unit)
}
case class Some[+A](get: A) extends Option[A]
case object None extends Option[Nothing]
Here is a hard-coded version of map
:
def map[B](f: A => B): Option[B] =
this match {
case None => None
case Some(a) => Some(f(a))
}
def getOrElse[B>:A](default: => B): B =
this match {
case None => default
case Some(a) => a
}
Implement flatMap
using map
& getOrElse
.
def flatMap1[B](f: A => Option[B]): Option[B] =
map(f) getOrElse None
Try
is a general-purpose function for converting from an exception-based API to a container-based API.
def Try[A](a: => A): Option[A] =
try { Some(a) }
catch { case e: Exception => None }
The argument here must be lazy, otherwise Try
will not function.
The None
returned is not informative about the exception thrown. Next lecture's type Either
fixes this.
Note that the Scala std lib has a type constructor Try[_]
, but it is not monadic:
import scala.util.{Try, Success}
def verify(n: Int): Try[Int] =
if (n == 0) sys.error("nope") else Success(n)
val x = Try(0).flatMap(verify)
//x: Try[Int] = Failure(RuntimeException: nope)
val y = verify(0)
//RuntimeException: nope
We'll see another way to wrap a Try
when we discuss the Validated
applicative:
def fromTry[A](t: Try[A]): Validated[Throwable, A] =
t match {
case Failure(e) => invalid(e)
case Success(v) => valid(v)
}
def fromEither[A, B](e: Either[A, B]): Validated[A, B] =
e.fold(invalid, valid)
def fromOption[A, B](o: Option[B], ifNone: => A): Validated[A, B] =
o.fold(invalid[A, B](ifNone))(valid)
Option
in Catsimport cats.syntax.option._
Some(3)
//res0: Some[Int] = Some(3)
3.some
//res1: Option[Int] = Some(3)
3.some.map { (i: Int) => i+2 }
//res2: Option[Int] = Some(5)
for {
i <- List(0)
} yield(i + 1)
//res0: List[Int] = List(1)
List(0) map {i => i + 1}
//res1: List[Int] = List(1)
for {
i <- List(1)
j <- List(2)
} yield (i, j)
//res2: List[(Int, Int)] = List((1,2))
List(1) flatMap {
i => List(2) map {
j => (i, j)
}
}
//res3: List[(Int, Int)] = List((1,2))
for {
x <- 3.some
y <- 4.some
} yield x+y
//res0: Option[Int] = Some(7)
3.some.flatMap { x =>
4.some.map { y => x+y }
}
//res1: Option[Int] = Some(7)
val error = none
for {
x <- 3.some
y <- error
} yield (x,y)
//res0: Option[(Int, Nothing)] = None
for {
i <- List(1)
j <- List()
} yield (i,j)
//res1: List[(Int, Nothing)] = List()
Implement a map2
function with the following signature:
def map2[A,B,C](a: Option[A], b: Option[B])
(f: (A, B) => C): Option[C]
def map2[A,B,C](a: Option[A], b: Option[B])
(f: (A, B) => C): Option[C] =
a flatMap (aa => b map (bb => f(aa, bb)))
We could also implement map2
with a for-comprehension:
def map2[A,B,C](a: Option[A], b: Option[B])
(f: (A, B) => C): Option[C] =
for {
i <- a
j <- b
} yield f(i, j)
Provided a function mean
,
mean(xs: Seq[Double]): Option[Double] = ...
implement a variance
function with the following signature:
variance(xs: Seq[Double]): Option[Double]
def variance(xs: Seq[Double]): Option[Double] =
mean(xs) flatMap { m =>
mean(xs.map(x => math.pow(x - m, 2)))
}
sequence
and traverse
These are two combinators we will see repeatedly throughout the course:
def sequence[A](a: List[Option[A]]): Option[List[A]]
def traverse[A, B](a: List[A])(f: A => Option[B]): Option[List[B]]
sequence(List(Some(1), Some(2)))
//res0: Option[List[Int]] = Some(List(1, 2))
sequence(List(Some(1), None))
//res1: Option[List[Int]] = None
def foo(x: Int) = if (x==2) None else Some(x)
traverse(List(1,2,3))(foo)
//res2: Option[List[Int]] = None
traverse(List(1,3,4))(foo)
//res3: Option[List[Int]] = Some(List(1, 3, 4))
Implement sequence[A](a: List[Option[A]]): Option[List[A]]
.
def sequence[A](a: List[Option[A]]):
Option[List[A]] =
a.foldRight[Option[List[A]]]
(Some(Nil))
((x,y) => map2(x,y)(_ :: _))
def sequence1[A](a: List[Option[A]]):
Option[List[A]] =
a match {
case Nil => Some(Nil)
case h :: t => h flatMap (hh => sequence1(t) map (hh :: _))
}
traverse
can be trivially implemented with sequence
and map
.
def traverse[A, B](a: List[A])(f: A => Option[B]):
Option[List[B]] =
sequence a.map(f)
However this implementation is inefficient.
Reimplement traverse
so that it traverses the list only once.
def traverse[A, B] (a: List[A])(f: A => Option[B]):
Option[List[B]] =
a.foldRight[Option[List[B]]] (Some(Nil)) {
(h,t) => map2(f(h),t)(_ :: _)
}
def traverse1[A, B] (a: List[A])(f: A => Option[B]):
Option[List[B]] =
a match {
case Nil => Some(Nil)
case h :: t => f(h) flatMap {
x: B => traverse(t)(f) map { xt => x :: xt }
}
}
def traverse2[A, B] (a: List[A])(
f: A => Option[B]): Option[List[B]] =
a match {
case Nil => Some(Nil)
case h :: t => traverse(t)(f) flatMap { xt =>
f(h) map { x: B => x :: xt }
}
}
The fact that traverse1
and traverse2
are equivalent is a simple result of the Monad laws.
Note also that traverse
is not equivalent to the other two, it uses only map2
and is therefore an applicative functor.
We'll discuss this more in the weeks to come.
Have a look at Xor
and Validated
in Cats.
Lecture 4: Options | 1 |
---|---|
Imperative Error Handling | 2 |
Flagged Arguments | 3 |
Sentinel Values | 4 |
Exceptions | 5 |
- | 6 |
- | 7 |
Failure is an Option |
8 |
Application: Computing a mean | 9 |
- | 10 |
- | 11 |
- | 12 |
- | 13 |
- | 14 |
- | 15 |
Exercise | 16 |
- | 17 |
Try | 18 |
- | 19 |
- | 20 |
- | 21 |
Option in Cats |
22 |
Reduction of for loops | 23 |
- | 24 |
'Looping' over Options | 25 |
- | 26 |
- | 27 |
Exercise | 28 |
- | 29 |
- | 30 |
Exercise | 31 |
- | 32 |
sequence and traverse |
33 |
- | 34 |
- | 35 |
Exercise | 36 |
- | 37 |
- | 38 |
- | 39 |
Exercise | 40 |
- | 41 |
- | 42 |
- | 43 |
- | 44 |
Homework | 45 |
Links | 46 |
Table of Contents | t |
---|---|
Exposé | ESC |
Full screen slides | e |
Presenter View | p |
Source Files | s |
Slide Numbers | n |
Toggle screen blanking | b |
Show/hide slide context | c |
Notes | 2 |
Help | h |