3
votes

Suppose I have two functions to get orders and order items:

def getOrders(): Option[List[Int]] = ...
def getOrderItems(orderId: Int): Option[List[Int]] = ...

Note that both functions return Option[List] since each function may fail.

Now I would like to get Option of List of all order items as follows:

  • return Some[List] if both functions return Some and
  • None if any of them returns None.

I tried to compose these functions with for (see below) but it did not work.

val allOrderItems = for {
  orderIds   <- getOrders();
  orderId    <- orderIds;
  orderItems <- getOrderItems(orderId)
} yield orderItems

How can I build a function getAllOrderItems():Option[List[Int]] using functions getOrders and getOrderItems ?

2
What do you mean with "it did not work"? Did you get an error, or different results than what you expected? What did you expect and how does this differ from what it actually does?Jesper
The Option and the List monads are not compatible. You need to use monad transformers, or call toList on the Options (or breakOut with the proper type might also work).Gábor Bakos
@GáborBakos Thank you. monad transformer looks like an advanced topic. Besides, I would need scalaz for that. How would you suggest do that with breakOut ?Michael
@Michael In that case would probably just call toList after getOrder(), like getOrder().toList. I am not sure breakOut can be used here, but that was designed for similar use cases. (Though in this case toList seems to be easier.)Gábor Bakos
@GáborBakos Thanks. toList looks really simple.Michael

2 Answers

6
votes

You really want to be able to turn the middle two layers of Option[List[Option[List[Int]]]] inside out, so that you can get the options and lists next to each other. This operation is called sequencing, and it's provided by Scalaz:

import scalaz._, Scalaz._

val items: Option[List[Int]] =
  getOrders.flatMap(_.map(getOrderItems).sequence).map(_.flatten)

You could equivalently use traverse, which combines the map and sequence operations:

val items: Option[List[Int]] =
  getOrders.flatMap(_ traverse getOrderItems).map(_.flatten)

If you don't want to use Scalaz, you could write your own (less polymorphic) sequence:

def sequence[A](xs: List[Option[A]]) = xs.foldRight(Some(Nil): Option[List[A]]) {
  case (Some(h), Some(t)) => Some(h :: t)
  case _ => None
}

And then:

val items: Option[List[Int]] = getOrders.flatMap(
  orderIds => sequence(orderIds.map(getOrderItems))
).map(_.flatten)

The monad transformation solution is actually pretty straightforward as well (if you're willing to use Scalaz):

val items: Option[List[Int]] = (
  for {
    orderId <- ListT(getOrders)
    itemId  <- ListT(getOrderItems(orderId))
  } yield itemId
).underlying

The nice thing about this approach is that you don't have to think about where you need to flatten, sequence, etc.—the plain old monadic operations do exactly what you want.

2
votes

The simplest modification I could think of is as below:

for{
    orderId <- getOrders.getOrElse(Nil)
    items <- getOrderItems(orderId)
} yield items

The for comprehension uses the first statement to determins the rest the types. For instance in the above the type List[Int] would be infered and this is different from Option[List[Int]].