Code not compiling when Unit-returning SAM and overloaded methods involved

I recently opened a ticket in dotty https://github.com/lampepfl/dotty/issues/13549. This was closed as “won’t fix” as the proposed change was breaking some community build projects.
I understand the reasoning to not proceed with that fix. But I want to point out the current inconsistency of the Scala3 compiler.
Given the following example (listing 1):

object SimpleReproducer {
  
  myMethod("Hello", () => 3) // compiles

  def myMethod(message: String, executable: Executable): Unit = ???

  @FunctionalInterface
  trait Executable {
    @throws[Throwable]
    def execute(): Unit
  }

}

The inconsistency I currently see is that the this call will sucessfully compile myMethod("Hello", () => 3) while if we add a new overload for myMethod, the code doesn’t compile anymore, see the following (listing 2):

object SimpleReproducer {
  
  myMethod("Hello", () => 3) // doesn't compile

  def myMethod(message: String, executable: Executable): Unit = ???
  def myMethod(occurrences: Int, executable: Executable): Unit = ???

  @FunctionalInterface
  trait Executable {
    @throws[Throwable]
    def execute(): Unit
  }

}

I think consistency is key to understanding why the code compiles or fails to do so. For this reason given that the option to make “listing 2” compile is not possible as it breaks some community build projects, I would argue that the right move is to make “listing 1” not compile as well.

It is often the case that overloading a method cripples type inference, though there have been made some improvements in that area. So that to me is not a good argument to make certain code not compile. However there may be other arguments in favor of limiting automatic ()-insertion more than it currently is.

For instance

import some.fp.api.Task

val task: Task[Unit] = 
  Task {
    println("starting inner task")
    Task(println("running inner task"))
  } 
  // oops forgot to flatten, but compiles anyway

task.run()
// out: starting inner task
1 Like

Yes, that’s inevitable in general because overloading means we’re typing an argument without knowing the type of the corresponding parameter. Now, in this particular issue it turns out that all overloads use the same parameter type at the position where the lambda is passed, so we could try to type it with a known expected type, and in fact we already have logic to do so when the parameter type is a function type: dotty/compiler/src/dotty/tools/dotc/typer/Applications.scala at aaac006a58c3f5d7fbd64d3a1a19261b51dfecf5 · lampepfl/dotty · GitHub. It 's possible this logic could be extended to handle the case where the parameter type is a SAM type too.

1 Like

My view on this is that I think the auto-insertion of () makes sense when a Java method is called but not a Scala one. Arguably in idiomatic Scala code you should be explicit in inserting the () if the last expression doesn’t return a Unit due to the “functional” nature for Scala (especially the last expression in a block is a return statment). On the other hand with Java this code is quite common practice (note that in the original issue myMethod is actually assertThrows which is a JUnit Java method as is Executable, this was only redefined in Scala to make a minimal reproduction case).

Furthermore Scala has Any which can be used instead of Unit if you really want to ignore the type of the last expression in the block. Typically in Scala if you want to define assertThrows, you would do something like this

def assertThrows(expr: () => Any)

Since if you are checking that an expression throws something you don’t want to care about the last type in the expression. Java has no equivalent of Any so its not really possible to do there, you only have void i.e. Unit which is why its typical there.

This would also fix your problem with the Task example, since Task would be a Scala written method then we wouldn’t auto insert the () which would cause a compiler error due to forgetting to compose over Task by using flatten/flatMap etc etc.

2 Likes