Alex

Chapter 4

Motivation

def f(i: Int): Int = 
  val y: Int = throw Exception("fail")
  try
    val x = 42 + 5
    x + y
  catch
    case e: Exception => 43

Option

enum Option[+A]:
  case Some(get: A)
  case None
enum Option[+A]:
  case Some(get: A)
  case None

  def map[B](f: A => B): Option[B] // apply f if option is not None
  def flatMap[B](f: A => Option[B]): Option[B] // apply f (which may fail) if option is not None
  def getOrElse[B >: A](default: => B): B // get option, or default value (which must be a supertype of A)
  def orElse[B >: A](ob: => Option[B]): Option[B] // get option, or 

Aside - By-name Parameters

Aside - More on Variance

Implementing the Option Methods

enum MyOption[+A]:
  case Some(get: A)
  case None

  def map[B](f: A => B): MyOption[B] = this match {
    case Some(a) => Some(f(a))
    case None => None
  }

  def getOrElse[B >: A](default: => B): B = this match {
    case Some(a) => a
    case None => default
  }

  def flatMap[B](f: A => MyOption[B]): MyOption[B] = map(f).getOrElse(None)

  // first map will make Some(Some(a)) | None; then getOrElse will extract some(a) or ob if None
  def orElse[B >: A](ob: => MyOption[B]): MyOption[B] = map(Some(_)).getOrElse(ob)

  // this could also just be done with a match
  def filter(f: A => Boolean): MyOption[A] = flatMap(a => if f(a) then Some(a) else None)
case class Employee(
  name: String,
  department: String,
  manager: Option[Employee])
def lookupByName(name: String): Option[Employee] = ...

lookupByName("Joe").map(_.department) // will return joe's department if joe is listed as an employee (from lookupByName), else None. Map is used since an employee will always have a department

lookupByName("Joe").flatMap(_.manager) // will return joe's managed if Joe has a manager, else None if Joe is not an employee or doesn't have a manager. FlatMap is used since an employee doesn't always have a manager

Exercise

def mean(xs: Seq[Double]): MyOption[Double] =
  if xs.isEmpty then None
  else Some(xs.sum / xs.length)

// we use flatmap to short circuit the computation if the mean is None
def variance(xs: Seq[Double]): MyOption[Double] =
  val m = mean(xs)
  m.flatMap(m => mean(xs.map(x => math.pow(x-m, 2))))

Option Composition and Lifting

def lift[A, B](f: A => B): Option[A] => Option[B] =
  a => a.map(f)

lift(math.abs) // returns lifted abs function
// this could be done with pattern matching but we can map/flatmap for nicer implementation
def map2[A, B, C](a: MyOption[A], b: MyOption[B])(f: (A, B) => C): MyOption[C] =
  a.flatMap(_a => b.map(_b => f(_a, _b)))

Aside on Parameter Lists

map2(oa, ob): (a, b) =>
  a + b

// or 

map2(oa, ob) { (a, b) =>
  a + b
  }

// are both valid usages and can only be done with multiple parameter lists

Exercise 4.4

def sequence[A](as: MyList[MyOption[A]]): MyOption[MyList[A]] =
  foldRight(as, Some(Nil), (a, acc) => map2(a, acc)(Cons(_,_)))

Exercise 4.5

// naive implementation - uses sequence then map which loops over the list twice
def traverse[A, B](as: MyList[A])(f: A => MyOption[B]): MyOption[MyList[B]] =
  sequence(map(as, f))

// but we can do better, using a similar strategy as with sequence itself
def _traverse[A, B](as: MyList[A])(f: A => MyOption[B]): MyOption[MyList[B]] =
  foldRight(as, Some(Nil), (a: A, acc) => map2(f(a), acc)(Cons(_,_)))

For Comprehensions

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)

// can be converted to:
def map2[A, B, C](a: Option[A], b: Option[B])(f: (A, B) => C): Option[C] =
  for
    aa <- a
    bb <- b
  yield f(aa, bb)

Either

enum Either[+E, +A]:
  case Left(value: E)
  case Right(value: A)
def safeDiv(x: Int, y: Int): Either[Throwable, Int] =
  try Right(x / y)
  catch case NonFatal(t) => Left(t)

// we can extract this logic into a function itself:
def catchNonFatal(a: => A): Either[Throwable, A] =
  try Right(a) // uses lazy evaluation; a will not be evaluated as an argument and so the exception will be thrown in this function 
  catch case NonFatal(t) => Left(t)

Either Methods (Exercise 4.6)

enum MyEither[+E, +A]:
  case Left(value: E)
  case Right(value: A)

  def map[B](f: A => B): MyEither[E, B] = this match {
    case Right(value) => Right(f(value))
    case Left(value) => Left(value)
  }

  def flatMap[EE >: E, B](f: A => MyEither[EE, B]): MyEither[EE, B] = this match {
    case Left(value) => Left(value)
    case Right(value) => f(value)
  }

  def orElse[EE >: E,B >: A](b: => MyEither[EE, B]): MyEither[EE, B] = this match {
    case Left(_) => b
    case Right(value) => Right(value)
  }

  // this could equally be done with a for comprehension
  def map2[EE >: E, B, C](that: MyEither[EE, B])(f: (A, B) => C): MyEither[EE, C] =
    this.flatMap(_this => that.map(_that => f(_this, _that)))

Either Sequence and Traverse

def sequence[E, A](as: MyList[MyEither[E, A]]): MyEither[E, MyList[A]] =
  foldRight(as, MyEither.Right(Nil): MyEither[E, MyList[A]], (a: MyEither[E, A], acc) => a.map2(acc)(Cons(_, _)))

def traverse[E, A, B](as: MyList[A])(f: A => MyEither[E, B]): MyEither[E, MyList[B]] =
  foldRight(as, MyEither.Right(Nil), (a: A, acc: MyEither[E, MyList[B]]) => f(a).map2(acc)(Cons(_, _)))

Accumulating Errors

MyEither.Left("Uh oh").map2(MyEither.Left("Stinky"))((_,_)) // will return only Left("Uh oh"), ignoring the stinky error
def map2Both[E, A, B, C](
    a: MyEither[E, A],
    b: MyEither[E, B],
    f: (A, B) => C): MyEither[MyList[E], C] =
  (a, b) match {
    // it's a bit verbose but we handle every possible combination of values and errors:
    case (MyEither.Right(aa), MyEither.Right(bb)) => MyEither.Right(f(aa, bb))
    case (MyEither.Left(e), MyEither.Right(_)) => MyEither.Left(MyList(e))
    case (MyEither.Right(_), MyEither.Left(e)) => MyEither.Left(MyList(e))
    case (MyEither.Left(e1), MyEither.Left(e2)) => MyEither.Left(MyList(e1, e2))
  }