Why is Option not Traversable?

There may be challenges around having flatMap return an Option rather than a Traversable. Though that predates the whole 2.8 CanBuildFrom machinery.

The question was asked once before on the mailing list but didn't elicit a response.

Here is an illustration:

sealed trait OptionX[+A] extends Traversable[A] {
  def foreach[U](f: (A) => U): Unit = if (!isEmpty) f(get)
  def get: A
  def isDefined: Boolean
  def getOrElse[B >: A](default: => B): B
}

case class SomeX[+A](a: A) extends OptionX[A] {
  override def isEmpty = false
  def get = a
  def isDefined = true
  def getOrElse[B >: A](default: => B) = a
}

case object NoneX extends OptionX[Nothing] {
  override def isEmpty = true
  def get = sys.error("none")
  def isDefined = false
  def getOrElse[B](default: => B) = default
}

object O extends App {
  val s: OptionX[Int] = SomeX(1)
  val n: OptionX[Int] = NoneX
  s.foreach(i => println("some " + i))
  n.foreach(i => println("should not print " + i))
  println(s.map(_ + "!"))
}

The last line returns a List("1!") instead of Option. May be somebody can come up with a CanBuildFrom that would yield an SomeX("1!"). My attempt did not succeed:

object OptionX {
  implicit def canBuildFrom[Elem] = new CanBuildFrom[Traversable[_], Elem, OptionX[Elem]] {
    def builder() = new Builder[Elem, OptionX[Elem]] {
      var current: OptionX[Elem] = NoneX
      def +=(elem: Elem): this.type = {
        if (current.isDefined) sys.error("already defined")
        else current = SomeX(elem)
        this
      }
      def clear() { current = NoneX }
      def result(): OptionX[Elem] = current
    }
    def apply() = builder()
    def apply(from: Traversable[_]) = builder()
  }
}

I need to pass the implicit explicitly:

scala> import o._
import o._

scala> val s: OptionX[Int] = SomeX(1)
s: o.OptionX[Int] = SomeX(1)

scala> s.map(_+1)(OptionX.canBuildFrom[Int])
res1: o.OptionX[Int] = SomeX(2)

scala> s.map(_+1)
res2: Traversable[Int] = List(2)

Edit:

So I was able to work around the issue and have SomeX(1).map(1+) return an OptionX by having OptionX extend TraversableLike[A, OptionX[A]] and overriding newBuilder.

But then I get runtime errors on SomeX(1) ++ SomeX(2) or for (i <- SomeX(1); j <- List(1,2)) yield (i+j). So I don't think it's possible have option extend Traversable and do something sane in terms of returning the most specific type.

Beyond feasibility, coding style wise, I'm not sure it's a good thing to have Option behave like a Traversable in all circumstances. Option represent values that are not always defined, while Traversable defines methods for collections that can have multiple elements in it like drop(n), splitAt(n), take(n), ++. Although it would offer convenience if Option was also a Traversable, I think it may make intent less clear.

Using a toSeq where necessary seems like a painless way to indicate that I want my option to behave like a Traversable. And for certain recurring use cases, there is the option2Iterable implicit conversion - so for instance this already works (they all return List(1,2)):

  • List(Option(1), Option(2), None).flatten
  • for (i <- List(0,1); j <- Some(1)) yield (i+j)
  • Some(1) ++ Some(2)

It is not Traversable because you can't implement a scala.collection.mutable.Builder for it.

Well, it could be a Traversable even so, but that would result in a lot of methods that return Option now returning Traversable instead. If you want to see what methods are these, just look at the methods that take a CanBuildFrom parameter.

Let's take your sample code to demonstrate why:

Seq(Set(1,3,2),Seq(4),Option(5)).flatten

That ought to be equal to:

Seq(1, 2, 3, 4, 5)

Now, let's consider this alternative:

Option(Set(1,3,2),Seq(4),Option(5)).flatten

What's the value of that?