Implementing functional combinators for Scala Options
Map
def map[A, B](f: A => B)(opt: Option[A]): Option[B] = opt match { case Some(x) => Some(f(x)) case None => None } def map2[A,B](f: A => B)(opt: Option[A]): Option[B] = { opt flatMap (a => Some(f(a))) }
getOrElse
def getOrElse[A, B >: A](default: => B)(opt: Option[A]): B = opt match { case Some(x) => x case None => default }
flatMap
def flatMap[A, B](f: A => Option[B])(opt: Option[A]): Option[B] = opt match { case Some(x) => f(x) case None => None } def flatMap2[A, B](f: A => Option[B])(opt: Option[A]): Option[B] = { opt map f getOrElse None }
Filter
def filter[A](f: A => Boolean)(opt: Option[A]): Option[A] = opt match { case Some(x) => if (f(x)) Some(x) else None case None => None } def filter2[A](f: A => Boolean)(opt: Option[A]): Option[A] = { opt flatMap (a => if (f(a)) Some(a) else None) }
It’s fairly interesting to notice how my first attempt to write this was flawed:
def filter_wrong[A](f: A => Boolean)(opt: Option[A]): Option[A] = { opt map (a => if (f(a)) a else None) }
This is wrong because I’m passing to map a function that returns two different types: A in one case and None in the other, so that the return type is Option[Any] rather than Option[B].
Fold: the thing is to keep in mind is that folding an Option is the equivalent of chaining map and getOrElse:
Some(1) map identity getOrElse 10 Some(1).fold(10)(identity) None.fold(10)(identity)
the implementation is therefore straightforward:
def foldOption[A, B](z: B)(op: A => B)(opt: Option[A]): B = opt match { case Some(a) => op(a) case None => z }
mapCombine is just a way to showcase how for comprehensions are just syntactic sugar for chaining maps and flatMaps:
def mapCombine[A,B,C](a: Option[A], b: Option[B])(f: (A,B) => C): Option[C] = { for { aa <- a bb <- b } yield f(aa,bb) } def mapCombine2[A,B,C](a: Option[A], b: Option[B])(f: (A,B) => C): Option[C] = { a flatMap { aa => b map { bb => f(aa,bb) } } }
Last but not least, something more interesting, sequence and traverse.
def sequence[A](as: List[Option[A]]): Option[List[A]] = { as.foldRight[Option[List[A]]](Some(Nil)){ (maybeA, maybeAs) => maybeA match { case Some(a) => maybeAs map (as => a :: as) case None => None } } }
How this works: it starts with an empty list of as, our accumulator maybeAs; then it starts looping through List[Option[A]].
If we have an a, we add it to the accumulator;
If we don’t have an a, the accumulator becomes None, so that for all the next maybeA it will be None and the line maybeAs map ...
will actually be None map ...
, thus exhausting the list and eventually returning None.
Traverse can be easily implemented using sequence:
def traverse[A,B](as: List[A])(f: A => Option[B]): Option[List[B]] = { sequence(as map f) // where (as map f) is a List[Option[B]] }
Or less easily implemented from scratch:
def traverse2[A,B](as: List[A])(f: A => Option[B]): Option[List[B]] = { as.foldRight[Option[List[B]]](Some(Nil)) { (a, maybeAcc) => f(a) match { case Some(v) => maybeAcc map (acc => v :: acc) case None => None } } }
This is slightly inefficient because we’re computing f(a) for every a in as, even if the accumulator is set to None. This leads to an obvious improvement:
def traverse3[A,B](as: List[A])(f: A => Option[B]): Option[List[B]] = { as.foldRight[Option[List[B]]](Some(Nil)) { (a, maybeAcc) => maybeAcc match { case Some(acc) => f(a) map (aa => aa :: acc) case None => None } } }
Finally, we can reimplement sequence using traverse:
def seqViaTraverse[A](as: List[Option[A]]): Option[List[A]] = { traverse3(as)(identity) }