Why does Scala Option.tapEach return Iterable, not Option?

Viewed 275

The scaladoc of Option.tapEach states "returns: The same logical collection as this" just as expected for an operation named after tap & foreach. However, it does not return an Option but an Iterable backed by a List:

scala> import scala.util.chaining._

scala> Option(5).tap(_.foreach(_ => ()))
val res0: Option[Int] = Some(5)

scala> Option(5).tapEach(_ => ())
val res1: Iterable[Int] = List(5)

(Verified for Scala 2.13.5 and 3.0.0-RC1)

Is there a good reason to return Iterable instead of Option, or has this just been overlooked (and might be fixed eventually)?

1 Answers

It seems whether Option is considered a full collection is a bit of a can of worms as indicated by the discussion at Make Option extend IterableOnce #8038. I think the relevant comment is

So it can definitely be a IterableOnce because you can get an iterator of zero to one elements. But it can't be a Iterable because you you can't implement fromSpecific(c: IterableOnce[A]): K without throwing away data.

however tapEach uses fromSpecific in its definition

override def tapEach[U](f: A => U): C = fromSpecific(new View.Map(this, { (a: A) => f(a); a })

So key to remember is Option since Scala 2.13 is an IterableOnce but not a full Iterable. IterableOnce is smaller compared to Iterable, so if a capability is needed from Iterable it is provided via implicit conversion as per docs

This member is added by an implicit conversion from Option[A] to Iterable[A] performed by method option2Iterable in scala.Option.

that is

option2iterable(Option(5)).tapEach(_ => ())

hence the Iterable[Int] return type.

Also consider the following note

Many of the methods in here are duplicative with those in the Traversable hierarchy, but they are duplicated for a reason: the implicit conversion tends to leave one with an Iterable in situations where one could have retained an Option.

so contributors would have to bake a specialised version in Option to preserve the type, or perhaps we could provide our own specialised extension implementation, something like

scala> implicit class OptionTapOps[A](v: Option[A]) {
     |   def tapEach[B](f: A => B): Option[A] = { v.foreach(f); v }
     | }
class OptionTapOps

scala> Option(5).tapEach(_ => ())
val res11: Option[Int] = Some(5)
Related