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 Either[+E, +A]:
  case Left(value: E)
  case Right(value: A)

def map[B](f: A => B): Either[E, B]
def flatMap[EE >: E, B](f: A => Either[EE, B]): Either[EE, B]
def orElse[EE >: E,B >: A](b: => Either[EE, B]): Either[EE, B]
def map2[EE >: E, B, C](that: Either[EE, B])(f: (A, B) => C): Either[EE, C]