Type hint required for Runnable in one place but not in another?

In one place in my code I call two different Java methods that take the same SAM type - java.lang.Runnable - as an argument. For one call, I need to specify a type hint and for the other, I don’t.

What could be the reason for this?

For one call, I can simply pass an anonymous function:

foo(() => println("foo"))

And for the other, compilation fails unless I provide a type hint:

bar((() => println("bar")) : Runnable)

In the first case, the Java code being called is mine and in the other, it comes in via a dependency. So I thought it might have to do with one involving an older class file format or something like that.

But if I use javap -verbose and look at things, both cases seem to be the same.

The actual calls are directly after each other in my code:

hello.foo(() => println(("alpha")))

context.system.scheduler.schedule(2.seconds, 5.seconds,
  (() => println("beta")) : Runnable)(context.dispatcher)

The first call is to an example piece of Java that I wrote and works fine without the Runnable type hint.

The second call is to some Akka logic and if I leave out the Runnable type hint, compilation fails with:

type mismatch;
 found   : () => Unit
 required: Runnable
    context.system.scheduler.schedule(2.seconds, 5.seconds, () => println("foobar"))(context.dispatcher)

If I use javap things look fairly identical for both cases.

Here is the calling Scala code, first the call to foo and then the call to schedule:

minor version: 0
major version: 52

ScalaSig: length = 0x3 (unknown attribute)
 05 02 00

 #68 = Utf8               foo
 #69 = Utf8               (Ljava/lang/Runnable;)V
 #70 = NameAndType        #68:#69       // foo:(Ljava/lang/Runnable;)V
 #71 = InterfaceMethodref #67.#70       // com/example/HelloWorldSibling.foo:(Ljava/lang/Runnable;)V
...
#104 = Utf8               schedule
#105 = Utf8               (Lscala/concurrent/duration/FiniteDuration;Lscala/concurrent/duration/FiniteDuration;Ljava/lang/Runnable;Lscala/concurrent/ExecutionContext;)Lakka/actor/Cancellable;
#106 = NameAndType        #104:#105     // schedule:(Lscala/concurrent/duration/FiniteDuration;Lscala/concurrent/duration/FiniteDuration;Ljava/lang/Runnable;Lscala/concurrent/ExecutionContext;)Lakka/actor/Cancellable;
#107 = InterfaceMethodref #103.#106     // akka/actor/Scheduler.schedule:(Lscala/concurrent/duration/FiniteDuration;Lscala/concurrent/duration/FiniteDuration;Ljava/lang/Runnable;Lscala/concurrent/ExecutionContext;)Lakka/actor/Cancellable;

And then the two different Java methods:

1. The Java code written by me that doesn’t require a type hint:

minor version: 0
major version: 52

public abstract void foo(java.lang.Runnable);
  descriptor: (Ljava/lang/Runnable;)V

2. The Java code, provided by a dependency on Akka, that does require a type hint:

minor version: 0
major version: 52

public default akka.actor.Cancellable schedule(scala.concurrent.duration.FiniteDuration, scala.concurrent.duration.FiniteDuration, akka.actor.ActorRef, java.lang.Object, scala.concurrent.ExecutionContext, akka.actor.ActorRef);
  descriptor: (Lscala/concurrent/duration/FiniteDuration;Lscala/concurrent/duration/FiniteDuration;Lakka/actor/ActorRef;Ljava/lang/Object;Lscala/concurrent/ExecutionContext;Lakka/actor/ActorRef;)Lakka/actor/Cancellable;

If I’ve missed some crucial bit of the javap output just follow up and I’ll add it.

BTW the compiler version involved is the one that’s part of Scala 2.13.6.

Not sure what the exact cause is, but this should be due to some limitation of overload resolution - check the #schedule() overloads and compare:

def foo(s: Any, r: Runnable): Unit = ()
def foo(s: String, r: Runnable): Unit = ()
foo("x", () => ()) // type mismatch, compiles with type ascription
1 Like

Thanks, @sangamon - you’re right. If I look at the Scheduler class it has various overloaded methods involving runnables, some using multiple parameter lists and some not. E.g.:

def scheduleWithFixedDelay(FiniteDuration, FiniteDuration)(Runnable)
def scheduleWithFixedDelay(Duration, Duration)(Runnable)

def scheduleOnce(delay: FiniteDuration, Runnable)
def scheduleOnce(delay: Duration, Runnable)

For the overloaded methods where the runnable is split out into its own parameter list no type hint is required, for those where the runnable is in the same parameter list as the other arguments then a type hint is required:

// Valid both with and without type hint:
scheduler.scheduleWithFixedDelay(initialDelay, interval)
  ((() => println("beta")) : Runnable)
scheduler.scheduleWithFixedDelay(initialDelay, interval)
  (() => println("beta"))

// Only valid with type hint:
scheduler.scheduleOnce(initialDelay, (() => println("beta")) : Runnable)
scheduler.scheduleOnce(initialDelay, () => println("beta")) // Invalid.

However, the compiler error seems very odd:

type mismatch;
 found   : () => Unit
 required: Runnable
    scheduler.scheduleOnce(initialDelay, () => println("beta"))(ec)

It’s not the usual error you get when it can’t resolve between multiple overloaded methods. It knows exactly what type is involved, i.e. Runnable , and in other circumstances, it has no problem working out how to map the found type, i.e. () => Unit, to this type.

So this looks like a bug rather than some valid situation in which the compiler just has to give up. I.e. whatever logic that can map () => Unit to a SAM type like Runnable is being incorrectly skipped in this situation.

Note that the situation seems to have improved, as this works just fine with Scala 3:

2 Likes

You could file a bug report with the minimized code.

1 Like