1

I have written the following in Scala:

implicit class Appender[A, B](x: (Growable[A], Growable[B])) { def ++=(y: (TraversableOnce[A], TraversableOnce[B])): (Growable[A], Growable[B]) = (x._1 ++= y._1, x._2 ++= y._2) } 

But Scala doesn't accept x._2 ++= y._2, complaining that: Expression of type Growable.this.type doesn't conform to expected type Growable[B].

I also tried:

enter image description here

for which IntelliJ suggests a return type of (Growable.this.type, Growable.this.type), which leads to other problems.

What am I missing? What would be the correct way to add this ++= operation to a Tuple2 of 2 Growable?

Is there a way to write that kind of "lifting" but generalizing it to the operation (here ++=) too? That would be some implicit Lift that can transfer an operation from the type inside a pair to the pair type itself. Is that possible in Scala?

4
  • Try compiling with Scala compiler. It's just the type checker in the IDE sometimes doesn't understand perfectly normal Scala code. Commented Nov 12, 2018 at 1:37
  • It seems genuine (ie not just in IntelliJ) - the compiler refuses if I try to return (AA, AA) in the example with A and AA. Commented Nov 12, 2018 at 1:47
  • Your first snippet with x: (Growable[A], Growable[B]) compiles and works fine for me. Commented Nov 12, 2018 at 2:13
  • Not for me, because I have a specific subtype of Growable[A] (actually I have A = B), and the returned type (Growable.this.type, Growable.this.type) is not automatically casted to my specific subtype. Commented Nov 12, 2018 at 2:22

1 Answer 1

2

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) 
Sign up to request clarification or add additional context in comments.

4 Comments

Thanks! Bonus question: is it possible to generalize by abstracting out ++=, so that we can automatically pass any (operation that takes a Growable and a Traversable to a Growable) to (an operation that takes a pair of Growable, a pair of Traversable and returns a pair of Growable)? I'm lazy and don't want to write a def xyz for every function xyz of Growable :-)
I confirmed that your second solution works for me too. It is my choice: it seems a bit more elegant than the first one, although we shouldn't have to "assist the compiler", IMHO.
@Frank See if my edit answers your question. Also, I agree that we shouldn't have to assist the compiler, but I have an impression that those kinds of things are really non-trivial and may be approaching the limit of what is feasible to do with Scala's type system, which has to support OOP with subclasses, but also FP and all kinds of implicit resolution. I maybe wrong with that opinion though, and it's just an oversight.
thanks. The wrappers are not working in my context yet, but I'll continue working on it. I algo got in trouble when I defined an Appender1 with A only and Appender2 with types A and B, both with a pair on the LHS but with a single collection on the RHS for Appender1 for ++=, with the semantic that ++= adds the same things to both elements of the pair for Appender1. It seemed to get confused about ++= in that case.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.