javascalatwitterinteroptwitter-util

Java-Scala interop issue involving BoxedUnit/void return types


I have the same problem as in Java interoperability woes with Scala generics and boxing, but I don't think the solution there will work for me because it would require a modification to third-party code.

Specifically, from Java (say MyJavaClass) I'm trying to extend a Scala class (MyScalaClass) that itself extends com.twitter.app.App. App extends com.twitter.util.CloseAwaitably, and this in turn extends com.twitter.util.Awaitable.

Awaitable            // trait where `result` is defined
 ^-CloseAwaitably    // trait with some impl of `result`
   ^-App             // trait with no mention of `result`
     ^-MyScalaClass  // abstract class with no mention of `result`
       ^-MyJavaClass // just trying to get this guy to compile
                     // and it expects an impl of `result`

All that to say that when I finally get to extending MyScalaClass by writing MyJavaClass, I get

[ERROR] MyJavaClass.java:[11,8] MyJavaClass is not abstract and does not override abstract method result(com.twitter.util.Duration,com.twitter.util.Awaitable.CanAwait) in com.twitter.util.Awaitable

I figured I just have to implement result for some reason, so I do, in MyJavaClass:

public void result(com.twitter.util.Duration d,
                   com.twitter.util.Awaitable.CanAwait c)
{}

Now I get

[ERROR] return type void is not compatible with scala.runtime.BoxedUnit

Changing my method to

public BoxedUnit result(com.twitter.util.Duration d,
                        com.twitter.util.Awaitable.CanAwait c)
{return BoxedUnit.UNIT;}

results in

[ERROR] return type scala.runtime.BoxedUnit is not compatible with void

What the heck... So then I start googling. The answer to Java interoperability woes with Scala generics and boxing seems to say that Scala generates Java-incompatible bytecode in my situation, and that I could hack around the problem if I had control of some of the Twitter classes (I think CloseAwaitably) to genericize it with [U <: Unit] and return ().asInstanceOf[U] in the original method implementation to trick scalac into admitting the possibility of "some not-exactly-Unit type" (though I'm not totally clear on how this works).

I don't have control over the Twitter classes, only MyScalaClass and MyJavaClass. I don't actually care about the result method - I just want to be able to extend MyScalaClass from MyJavaClass, implementing some abstract methods I defined in MyScalaClass. For what it's worth, I'm using Scala 2.10.4, JDK 1.8, and (twitter) util-core_2.10 6.22.0, and in case this is relevant: I don't really know why it's requiring me to implement result in the first place. A Scala class inheriting from MyScalaClass doesn't have to implement that method, and builds just fine.

How can I get around this? Thanks for playing.


Solution

  • You can use a similar trick to get Scala's part right, but javac still gets confused because it's looking for things that are "wrong" rather than those that are right. (The JVM looks for what is right, so it runs fine.)

    The gory details are as follows.

    The Twitter hierarchy can be simplified as follows:

    package test
    
    trait A[+Z] { def z(): Z }
    trait B extends A[Unit] { def z() = () }
    trait C extends B {}
    

    Now, when you write your class, you don't just extend C. Instead, you

    package test
    
    abstract class D[U <: Unit] extends A[U] with C {
      override def z: U = (this: B).z.asInstanceOf[U]
    }
    
    abstract class E extends D[Unit] {}
    

    where E takes the place of MyScalaClass. (And z is result.)

    Now, with the extends-unit trick, all the signatures that Java could possibly want are present:

    $ javap test.B
    public interface test.B extends test.A{
        public abstract void z();
    }
    
    $ javap test.D
    public abstract class test.D extends java.lang.Object implements test.C{
        public scala.runtime.BoxedUnit z();
        public java.lang.Object z();
        public void z();
        public test.D();
    }
    

    void z is not abstract any more! In fact, nothing is.

    But javac still isn't happy.

    package test;
    
    public class J extends E {
      public J() {}
    }
    
    $ javac -cp /jvm/scala-library.jar:. J.java
    J.java:3: test.J is not abstract and does not override
              abstract method z() in test.B
    

    Um...javac, yes it does?

    And if we make J abstract itself, it explains what the real problem is:

    J.java:3: z() in test.D cannot implement z() in test.B; 
              attempting to use incompatible return type
    found   : scala.runtime.BoxedUnit
    required: void
    

    That is, if two methods differ only by return type, and it's not the Java-sanctioned generic Object vs something-else, javac won't find the correct one.

    This looks to me like a javac bug more than a scalac one. Unfortunately, it leaves you unable to implement the classes in Java.

    As a workaround, you can (awkwardly) use composition by linking two classes together.

    Java:

    package test;
    
    public interface I {
      public void doThing();
    }
    

    Then Scala:

    package test
    
    trait A[+Z] { def z(): Z }
    trait B extends A[Unit] { def z(): Unit = println("Ok") }
    trait C extends B {}
    
    class D extends C {
      private var myI: I
      def i = myI
      def bind(newI: I) { myI = newI }
      def doJavaThing = i.doThing
    }
    

    Then Java again:

    package test;
    
    public class J implements I {
      public static D d;
      public J(D myD) { d = myD; d.bind(this); }
      public void doThing() { System.out.println("Works!"); }
    }
    

    Which at leasts let you switch back and forth whenever you need to:

    scala> new test.J(new test.D)
    res0: test.J = test.J@18170f98
    
    scala> res0.doThing
    Works!
    
    scala> res0.asScala.doJavaThing
    Works!
    
    scala> res0.asScala.asJava.doThing
    Works!