I am new to FP and Scala and am reading the book Functional Programming in Scala. One of the exercises in Chapter 4 asks us to write a function called sequence
which would convert a List[Option[A]]
to an Option[List[A]]
. Here Option
is a reimplementation of the Option
provided by the Scala library. Here's the required code.
trait Option[+A] {
/* Function to convert Option[A] to Option[B] using the function passed as an argument */
def map[B](f: A => B): Option[B] = this match {
case None => None
case Some(v) => Some(f(v))
}
/* Function to get the value in `this` option object or return the default value provided. Here,
* `B >: A` denotes that the data type `B` is either a super-type of `A` or is `A`
*/
def getOrElse[B >: A](default: => B): B = this match {
case None => default
case Some(v) => v
}
/* Used to perform (nested) operations on `this` and aborts as soon as the first failure is
* encountered by returning `None`
*/
def flatMap[B](f: A => Option[B]): Option[B] = {
map(f).getOrElse(None)
}
}
case class Some[+A](get: A) extends Option[A] // used when the return value is defined
case object None extends Option[Nothing] // used when the return value is undefined
Now I tried a lot, but I had to look up the solution for writing sequence
, which is,
def sequence[A](l: List[Option[A]]): Option[List[A]] = l match {
case Nil => Some(Nil) // Or `None`. A design decision in my opinion
case h :: t => h.flatMap(hh => sequence(t).map(hh :: _))
}
I just want to make sure I understood the solution correctly. So here are my questions.
case Nil
correct? Is it really a design decision or is one way better than the other?case h :: t
, this is what I understood. We pass the value h
firstly to the anonymous function in flatMap
(as hh
) which invokes sequence
recursively. This recursive call of sequence
returns an Option
encapsulating the Option
s in t
. We invoke map
on this returned value and pass h
to the anonymous function (as hh
) which then creates a new List[A]
with the list returned by the recursive call as the tail and h
as the head. This value is then encapsulated in Option
by invoking Some
and returned.Is my understanding for the the second part correct? If yes, is there a better way to explain it?
It seems that sequence
is intended to return None
if any element in the list is None
, and return Some
of values in the list otherwise. So your intuition about the Nil
case is not correct -- Nil
is an empty list that contains no None
s, so the result should not be None
.
Let's take it one step at a time, from the inside out.
Suppose we have some variable optionList
of type Option[List[A]]
and some variable a
of type A
. What do we get when we call:
optionList.map(a :: _)
If optionList
is None
, then this will be None
. If optionList
contains a list, say list
, this will be Some(a :: list)
.
Now if for some variable option
of type Option[A]
, what do we get when we call:
option.flatMap(a => optionList.map(a :: _))
If option
is None
, then this will be None
. If option
contains a value, say a
, then this will be optionList.map(a :: _)
, which we figured out above (by the definition of flatMap
).
Now if we tie it together, we see that if any element is None
, then the recursive call is avoided and the whole result will be None
. If no element is None
, then the recursive call will keep appending the element's values, and the result will be Some
of the list's element's internal values.
It might be more clearer if you rewrite the inner part:
def sequence[A](l: List[Option[A]]): Option[List[A]] = l match {
case Nil => Some(Nil)
case h :: t => h match {
case None => None
case Some(head) => sequence(t) match {
case None => None
case Some(list) => Some(head :: list)
}
}
}
Or even less idiomatic, but maybe clarifying:
def sequence[A](l: List[Option[A]]): Option[List[A]] = l match {
case Nil => Some(Nil)
case h :: t =>
val restOfList = sequence(t)
if (h == None || restOfList == None) None else Some(h.get :: restOfList.get)
}
You could also rewrite this pretty naturally as a fold
without recursion, in case that is what's confusion you:
def sequence[A](l: List[Option[A]]) = (Option(List.empty[A]) /: l) {
case(Some(sofar), Some(value)) => Some(value :: sofar);
case(_, _) => None
}
I think I was trying to solve same question from the same book and came up with this. It works for me and looks pretty clear and consice
def sequence[A](a: List[Option[A]]): Option[List[A]] = {
a.foldLeft(Option(List[A]())) {
(prev, cur) => {
for {
p <- prev if prev != None
x <- cur
} yield x :: p
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With