15

Is there any rational for Option not being Traversable?

In Scala 2.9, Seq(Set(1,3,2),Seq(4),Option(5)).flatten doesn't compile and simply having it to implement the Traversable trait seams rational to me. If it's not the case, there must be something I don't see that don't allow it. What is it?

PS: While trying to understand, I achieved awful things that compile, like:

scala> Seq(Set(1,3,2),Seq(4),Map("one"->1, 2->"two")).flatten
res1: Seq[Any] = List(1, 3, 2, 4, (one,1), (2,two))

PS2: I know I can write: Seq(Set(1,3,2),Seq(4),Option(5).toSeq).flatten or other ugly thing.

PS3: There seams to be work in the last month to make Option look more like Traversable without implementing it: commit, another commit

shellholic
  • 5,974
  • 1
  • 20
  • 30
  • 2
    For language (or in this case library) design questions like this, it's usually best to ask the language (or library) designers directly. They are pretty responsive on the mailing list but only occasionally visit StackOverflow. – Jörg W Mittag Jan 03 '12 at 23:42
  • 5
    @JörgWMittag It would be nice to have a compiled answer on SO, though.. –  Jan 04 '12 at 10:39

5 Answers5

5

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)
huynhjl
  • 41,520
  • 14
  • 105
  • 158
  • 1
    IMO `Traversable` shouldn't define `++` or `drop` etc. `Traversable` should define as being able to traverse, not necessarily to build, or to alter. Surprisingly after these years it is still an issue – SwiftMango Oct 19 '18 at 18:07
4

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?

Daniel C. Sobral
  • 295,120
  • 86
  • 501
  • 681
  • 4
    `Option#apply` takes one parameter, and answer in that case is: `Option(Set(1,3,2)).flatten => Set(1,3,2)` – shellholic Jan 04 '12 at 23:36
1

The reason is that in some cases with implicits applied the type would get less precise. You would still have an Option value, but the static return type would be something like Iterable, e. g. not the “most precise” one.

soc
  • 27,983
  • 20
  • 111
  • 215
1

Perhaps I'm being dense, but I don't understand why anyone would need this. Furthermore, it would require None to be Traversable as well which seems semantically dubious.

They say a design is finished not when there is nothing left to add, but rather nothing left to take away. Which is not to say, of course, that the Scala standard library is perfect.

Connor Doyle
  • 1,812
  • 14
  • 22
  • Touché. However `Nil` and `None` mean quite different things. `Nil` extends `List[Nothing]` and is the value of `List()`. If `Nil` were not `Traversable` it would cause lots of problems :) – Connor Doyle Jan 06 '12 at 19:51
  • 1
    How about `Future.traverse(option)(f)` or `Future.sequence(option)`. I think you can equally argue that the quote from Antoine de Saint-Exupéry is an argument in favor of `Option` being `Traversable`. Right now we have "a `Traversable` is a thing that you can traverse over, except for an `Option` which has almost all of the behavior of a `Traversable` but that isn't one so you should have separate methods for `Option` and every other kind of collection." The thing we've added is an arbitrary distinction that doesn't logically exist. – Sarah G Aug 25 '17 at 23:17
0

As of Scala 2.13, Option now extends IterableOnce, which is basically the new TraversableOnce now that TraversableOnce is deprecated in favor of simpler type hierarchy. From the official scala website, Option is now defined as

sealed abstract class Option[+A] extends IterableOnce[A] with Product with Serializable
krismath
  • 1,879
  • 2
  • 23
  • 41