def f(i: Int): Int =
val y: Int = throw Exception("fail")
try
val x = 42 + 5
x + y
catch
case e: Exception => 43
f passed in)
if retVal == sentinel and so on)def max[A](xs: Seq[A])(greater: (A,A) => Boolean): A which returns the largest value in xs based on the greater function semantics, if xs is empty we can’t invent a sentinel of type A, nor return null since null is only valid for non-primitive types (and A could be primitive in this case, e.g. Double)Optionenum Option[+A]:
case Some(get: A)
case None
Option and other data types allow for factoring out of common patterns of error handling via higher order functionsOption type: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
getOrElse and orElse in the Option data type above are denoted like => B, this means this is a by-name parametercomputeIfAbsent method, and under the hood works roughly the same way!A, this means that for some B which is a subtype of A, an Option[B] is a subtype of Option[A]
Option can be seen as a producer of values of type AOption[Animal], we can safely pass in an Option[Dog] because Dog is at least an Animal (if of course Dog < Animal!)Dog Animal into an Option[Dog] through type-safe means, so the substitution is soundA in Option, since it can be seen as a producer of values of type Atrait SomeFunc[-A, +B]: def apply(a: A): Bvar variable in itList in Scala is covariant on its type since it is immutable (similar to Option)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)
flatMap is similar except the function used to transform can itself failcase 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
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 using calls to Map, FlatMap and/or Filter, then using getOrElse to do error handling at the endo.getOrElse(throw Exception("Uh oh")), to convert the None case back to an Exception
Option or Either rather than throwing an exceptionOption might imply it has to be used consistently across an entire codebase (similar to function colouring), but this isn’t the case since we can lift ordinary functions to become functions that operate on Option: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)))
Option[Option[C]] then use getOrElse, but this is basically the definition of flatMap anyway (map(g).getOrElse(None))(a: MyOption[A], b: MyOption[B]) and (f: (A, B) => C)map2(oa, ob)(_ + _)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
def sequence[A](as: MyList[MyOption[A]]): MyOption[MyList[A]] =
foldRight(as, Some(Nil), (a, acc) => map2(a, acc)(Cons(_,_)))
sequence using foldRight and map2// 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(_,_)))
map and flatMap is so common that there is syntactic sugar with the for comprehension
do notation, although with slightly different syntaxdef 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)
<- are desugared to flatMap, except the final binding and yield is converted to a call to mapEither data type is used to store more information on a failure rather than just None that Option holdsenum Either[+E, +A]:
case Left(value: E)
case Right(value: A)
Either data type has two data constructors, Left and RightLeft state represents failure; some error, and the Right represents the result of a successful computation
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)
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]