scala / scala3

The Scala 3 compiler, also known as Dotty.
https://dotty.epfl.ch
Apache License 2.0
5.83k stars 1.05k forks source link

Parameter untupling breaks for polymorphic expected type, but succeeds if type args are inferred for overloading #16595

Open andrzejressel opened 1 year ago

andrzejressel commented 1 year ago

Compiler version

3.2.1-RC2, 3.2.0, 3.1.3

Minimized code

First:

// https://scastie.scala-lang.org/EeOjjIYVQeuvChR2HVphtA
object A {
  def b[Z](f: () => Z): Unit = println("ABC")
  def b[A, Z](f: A => Z): Unit = println("XYZ")
}

A.b((a: String, b: Int) => "test")

Second:

// https://scastie.scala-lang.org/EeLLJHr5TtWDAiK5pUg3Og
object A {
  // def b[Z](f: () => Z): Unit = println("ABC")
  def b[A, Z](f: A => Z): Unit = println("XYZ")
}

A.b((a: String, b: Int) => "test")

Output

First: XYZ Second: Compiler error:

Found:    (String, Int) => String
Required: Any => String

Expectation

Since only XYZ is printed I expected the first method is not needed. However after removing it code no longer compiles.

Other notes

After adding explicit type parameters it works fine:

// https://scastie.scala-lang.org/uqh8YmSeQma8pjYebJLcew
object A {
  // def b[Z](f: () => Z): Unit = println("ABC")
  def b[A, Z](f: A => Z): Unit = println("XYZ")
}

A.b[(String, Int), String]((a: String, b: Int) => "test")
prolativ commented 1 year ago

Actually I'm quite surprised this works if the method if overloaded. Note that (a: String, b: Int) => "test" is lambda syntax for a function taking 2 parameters (a String and an Int) rather than a single parameter being a tuple. Also none of these examples worked in scala 2

andrzejressel commented 1 year ago

Recently I had very similar issue with zio.schema, I was refactoring and left something like

.decode(myObject, None) // None was used on some previous library

Where method in ZIO was something like

fun decode(a: A)

Instead of throwing error that I've used two arguments instead of one, Scala decided to pack them into Tuple. That wasn't the easiest thing to debug ...

SethTisue commented 1 year ago

fyi @som-snytt

som-snytt commented 1 year ago

The feature is "parameter untupling" https://dotty.epfl.ch/docs/reference/other-new-features/parameter-untupling.html

which says that an n-ary function literal will be adapted to a function taking a tuple-n. The tuple parameter is untupled and applied to the function args. This is what retired the idiom xs.map { case (i, j) => i+j }.

The ticket title could be: parameter untupling breaks for polymorphic expected type, but succeeds if type args are inferred for overloading. I guess that is due to the supercharged type inference when overloads take a function, which began in Scala 2 and improved in Scala 3.

I would expect the untupling adaptation to happen after type inference in the non-overloaded case, much the way auto-tupling (we're told) is now a "last resort". I say that only because both features contain the word tupling. Is auto-untupling somehow more sound than auto-tupling, and therefore is tried more eagerly? Oh wait, the problem is that it infers Any => B, so it doesn't get a chance to infer the tuple type (X, Y) => B.

I don't know if this explains my reverso problemo from earlier today: https://github.com/lampepfl/dotty/issues/16617

where auto-tupling breaks under overloading. Or maybe it is the same: some downstream adaptations are not considered during overloading or type inference. Here, you don't get untupling when inferring A => Z, there, you don't get tupling during overloading.

Perhaps just as "improved type inference" happens during overloading, improved un/tupling could happen also during overloading and type inference.