Your first snippet compiles and works fine for me, although IntelliJ underlines it.
The problem with this code though is that it loses information about the concrete types in the first pair. The result comes out as the opaque (Growable[A], Growable[B]).
To preserve the types you can use something similar to the second snippet. The problem is that the compiler seems unable to apply the implicit conversion to the pair of Growable subtypes. But if you try to create the Appender explicitly it works:
object Test { implicit class Appender[ A, GA <: Growable[A], B, GB <: Growable[B] ](x: (GA, GB)) { def ++=(y: (TraversableOnce[A], TraversableOnce[B])): (GA, GB) = { (x._1 ++= y._1, x._2 ++= y._2) } } def main(args: Array[String]): Unit = { val a = (ArrayBuffer(1,2,3), ArrayBuffer("a","b","c")) println(new Appender(a).++=((List(4,5,6), List("d","e","f")))) } }
You can assist the compiler by defining the parts of x with a combined type GA with Growable[A]. In that case the compiler detects and applies the implicit conversion correctly using the Growable[A] part of the declaration. And you still have access to the full type GA to return from the method:
object Test { implicit class Appender[ A, GA <: Growable[A], B, GB <: Growable[B] ](x: (GA with Growable[A], GB with Growable[B])) { def ++=(y: (TraversableOnce[A], TraversableOnce[B])): (GA, GB) = { (x._1 ++= y._1, x._2 ++= y._2) } } def main(args: Array[String]): Unit = { val a = (ArrayBuffer(1,2,3), ArrayBuffer("a","b","c")) println(a ++= (List(4,5,6), List("d","e","f"))) } }
Append:
As for your bonus question, I don't think there is a simple way to automate the creation of all methods. Maybe you can do something with macros, but I'm not sure. But I believe you can considerably simplify method definitions using some helper methods. There are not that many methods you can meaningfully define for Growable, so here is what I believe a full definition:
implicit class Appender[ A, GA <: Growable[A] ](x: (GA with Growable[A], GA with Growable[A])) { private def wrap[B, R](f: GA => B => R): ((B, B)) => (R, R) = y => (f(x._1)(y._1), f(x._2)(y._2)) val ++= = wrap(_.++=) val += = wrap(_.+=) def clear(): Unit = { x._1.clear() x._2.clear() } }
If you have some other operation
def myOpDef[A, GA <: Growable[A]](ga: GA, traversable: TraversableOnce[A]): GA = ???
you can also add it to the Appender definition (but it seems a bit tricky):
val myOp = wrap(ga => myOpDef(ga, _))
or
val myOp = wrap(myOpDef[A, GA].curried)
(AA, AA)in the example withAandAA.x: (Growable[A], Growable[B])compiles and works fine for me.Growable[A](actually I haveA=B), and the returned type(Growable.this.type, Growable.this.type)is not automatically casted to my specific subtype.