9

I'm implementing a generic algorithm to return a collection based on two other collections. The problem can be simplified to

def add[Repr <: Traversable[_]](coll1: Repr, coll2: Repr) = coll1 ++ coll2

The problem occurred when I applied the algorithm on a collection A I've defined as

class A[T] extends Iterable[(Int,T)] with IterableLike[(Int,T), A[T]] { ... }

i.e., the type parameter of A is not the same as for the inherited Iterable. Map uses a similar approach.

Example with Map:

scala> val m1 = Map("a" -> 1, "b" -> 1, "c" -> 1)
m1: scala.collection.immutable.Map[java.lang.String,Int] = Map(a -> 1, b -> 1, c -> 1)

scala> val m2 = Map("a" -> 2, "c" -> 1)
m2: scala.collection.immutable.Map[java.lang.String,Int] = Map(a -> 2, c -> 1)

Applying add with m1 and m2 as parameters results in a List:

scala> add(m1,m2)
res3: Traversable[Any] = List((a,1), (b,1), (c,1), (a,2), (c,1))

...while the wanted result would be similar to using the ++ method directly:

scala> m1 ++ m2
res0: scala.collection.immutable.Map[java.lang.String,Int] = Map(a -> 2, b -> 1, c -> 1)

This problem does not occur using a collection B defined as:

class B[T] extends Iterable[T] with IterableLike[T, B[T]] { ... }

e.g., Queue is implemented in a similar way.

Example with Queue:

scala> val q1 = Queue(9,2,5)
q1: scala.collection.immutable.Queue[Int] = Queue(9, 2, 5)

scala> val q2 = Queue(7,3,1)
q2: scala.collection.immutable.Queue[Int] = Queue(7, 3, 1)

Applying add on q1 and q2 gives the wanted result:

scala> add(q1,q2)
res4: Traversable[Any] = Queue(9, 2, 5, 7, 3, 1)

Question: Is there a way to implement add, such that the result will be the same as when using the ++ method directly, for all kinds of travesables (including collections implemented similar to Map)? I have been trying to implement an implicit CanBuildFrom in the companion object of class A, with no luck. It seems to me that the problem is with the algorithm, not the collection implementations since it does not work for Map either.

1 Answers1

9

Given that add is nothing more than an alias for TraversableLike.++, the first step is too look at ++'s signature:

trait TraversableLike[+A, +Repr] extends ... {
  ...
  def ++:[B >: A, That](that: TraversableOnce[B])(implicit bf: CanBuildFrom[Repr, B, That]): That
  ...
}

Then all you have to do is to turn this into the first parameter, and that into the second parameter:

import collection.TraversableLike
import collection.generic.CanBuildFrom
def add[A, Repr, B >: A, That](coll1: TraversableLike[A, Repr], coll2: TraversableOnce[B])(implicit bf: CanBuildFrom[Repr, B, That]): That = {
  coll1 ++ coll2
}

UPDATE: I also investigated into what you would need to do to have A behave properly with respect to add. As it stands (without doing anything special ), add applied on instances of A returns an Iterable instead of an A:

import collection.{IterableLike, TraversableLike}
// Dummy `A` implementation, for illustration
class A[T]( val inner: Seq[(Int, T)] ) extends Iterable[(Int,T)] with IterableLike[(Int,T), A[T]] {
  def iterator: Iterator[(Int, T)] = inner.iterator
  override protected[this] def newBuilder: scala.collection.mutable.Builder[(Int, T),A[T]] = ???
  def :+(elem: (Int, T) ): A[T] = new A[T]( inner :+ elem )
}
object A  {
  def apply[T]( elems: (Int, T)* ) = new A( elems )
}
val a1 = A( 1-> "one", 2 -> "two" )
val a2 = A( 3-> "three", 4 -> "four", 5 -> "five" )
add(a1, a2)

The result is:

res0: Iterable[(Int, String)] = List((1,one), (2,two), (3,three), (4,four), (5,five))

Here is what I cam up by tibkering with CanBuildFrom. I cannot warrant that this is the best example but it does work (by which I mean we get an A as result when calling add):

import collection.IterableLike
import collection.generic.CanBuildFrom
import collection.mutable.Builder

class A[T]( val inner: Seq[(Int, T)] ) extends Iterable[(Int,T)] with IterableLike[(Int,T), A[T]] {
  def iterator: Iterator[(Int, T)] = inner.iterator
  override protected[this] def newBuilder: scala.collection.mutable.Builder[(Int, T),A[T]] = new A.ABuilder[T]
  def :+(elem: (Int, T) ): A[T] = new A[T]( inner :+ elem )
}
object A  {
  private val _empty = new A[Nothing]( Nil )
  def empty[T]: A[T] = _empty.asInstanceOf[A[T]]
  def apply[T]( elems: (Int, T)* ) = new A( elems )

  class ABuilder[T] extends Builder[(Int,T), A[T]] {
    protected var elems: A[T] = empty
    def +=(x: (Int, T)): this.type = { elems = elems :+ x; this }
    def clear() { elems = empty }
    def result: A[T] = elems
  }

  implicit def canBuildFrom[T]: CanBuildFrom[A[_], (Int,T), A[T]] = new CanBuildFrom[A[_], (Int,T), A[T]] {
    def apply(from: A[_]) = apply()
    def apply() = new ABuilder[T]
  }
}

Now the result is:

res0: A[String] = ((1,one), (2,two), (3,three), (4,four), (5,five))
Régis Jean-Gilles
  • 32,541
  • 5
  • 83
  • 97