The problem above is that your implicit conversion collectionExtras
causes the obtained object to lose type information. In particular, in the solution above, the concrete collection type is lost because you're passing it an object of type Iterable[A]
- from this point on, the compiler no longer knows the real type of xs
. Although the builder factory CanBuildFrom
programatically ensures that the dynamic type of the collection is correct (you really get a Vector
), statically, the compiler knows only that zipWith
returns something that is an Iterable
.
To solve this problem, instead of having the implicit conversion take an Iterable[A]
, let it take an IterableLike[A, Repr]
. Why?
Iterable[A]
is usually declared as something like:
Iterable[A] extends IterableLike[A, Iterable[A]]
The difference with Iterable
is that this IterableLike[A, Repr]
keeps the concrete collection type as Repr
. Most concrete collections, in addition to mixing in Iterable[A]
, also mix in the trait IterableLike[A, Repr]
, replacing the Repr
with their concrete type, like below:
Vector[A] extends Iterable[A] with IterableLike[A, Vector[A]]
They can do this because type parameter Repr
is declared as covariant.
Long story short, using IterableLike
causes you implicit conversion to keep the concrete collection type information (that is Repr
) around and use it when you define zipWith
- note that the builder factory CanBuildFrom
will now contain Repr
instead of Iterable[A]
for the first type parameter, causing the appropriate implicit object to be resolved:
import collection._
import collection.generic._
implicit def collectionExtras[A, Repr](xs: IterableLike[A, Repr]) = new {
def zipWith[B, C, That](ys: Iterable[B])(f: (A, B) => C)(implicit cbf: CanBuildFrom[Repr, C, That]) = {
val builder = cbf(xs.repr)
val (i, j) = (xs.iterator, ys.iterator)
while(i.hasNext && j.hasNext) {
builder += f(i.next, j.next)
}
builder.result
}
}
Reading your question formulation more carefully ("How to write a zipWith method that returns the same type of collection as those passed to it?"), it seems to me that you want to have the same type of collection as those passed to zipWith
, not to the implicit conversion, that is the same type asys
.
Same reasons as before, see solution below:
import collection._
import collection.generic._
implicit def collectionExtras[A](xs: Iterable[A]) = new {
def zipWith[B, C, That, Repr](ys: IterableLike[B, Repr])(f: (A, B) => C)(implicit cbf: CanBuildFrom[Repr, C, That]) = {
val builder = cbf(ys.repr)
val (i, j) = (xs.iterator, ys.iterator)
while(i.hasNext && j.hasNext) {
builder += f(i.next, j.next)
}
builder.result
}
}
With results:
scala> immutable.Vector(2, 2, 2).zipWith(mutable.ArrayBuffer(4, 4, 4))(_ * _)
res1: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(8, 8, 8)
new { def foo = }
produces a value of a structural type, which is invoked through reflection; to avoid this, declare the signatures in a trait ZipWith, and return an instance of this trait. This applies to the question and to all solutions. – Cq