scala / scala3

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

Explicit typing of value returned by method causes inference failure #10640

Open hmf opened 3 years ago

hmf commented 3 years ago

Minimized code

Motivation: the goal is to have a single overloaded method (lets call it func3) that can be used to concatenate Tuples. The library user should also be able to concatenate an element to a tuple using the same method name. Examples include: func3(1, (2,3)), func3((1,2), 3), func3(1, 2) and func3((1,2), (3,3)).

The code below successfully does this:

  import scala.util.Not

  @targetName("func3_none")
  def func3[A,B](a : A, b : B)(implicit ev1: Not[A<:<Tuple], ev2: Not[B<:<Tuple]): (A,B) = (a,b)
  @targetName("func3_right")
  def func3[A,B<:Tuple](a : A, b : B)(implicit ev: Not[A<:<Tuple]): *:[A,B] = a *: b
  @targetName("func3_left")
  def func3[A<:Tuple,B](a : A, b : B)(implicit ev: Not[B<:<Tuple]): Tuple.Concat[A,Tuple1[B]] = a ++ Tuple1(b)
  def func31[A<:Tuple,B](a : A, b : B)(implicit ev: Not[B<:<Tuple]): Tuple.Concat[A,Tuple1[B]] = a ++ Tuple1(b)
  @targetName("func3_both")
  def func3[A<:Tuple,B<:Tuple](a : A, b : B): Tuple.Concat[A,B] = a ++ b

    val d4: (Int, Int) = func3(1, 2)
    val d6: *:[Int, *:[Int, EmptyTuple]] = func3(1, 2)
    val d8: *:[Int, *:[Int, *:[Int, *:[Int, EmptyTuple]]]] = func3(1,(2,3,4))
    val d10_1: *:[Int, *:[Int, *:[Int, *:[Int, EmptyTuple]]]] = (1,2,3,4)
    val d10_2: *:[Int, *:[Double, *:[String, *:[Char, EmptyTuple]]]] = (1,2.0,"3") ++ Tuple1('4')
    val d10_3a: (Int, Double, String, Char) = func31((1,2.0,"3"), '4')
    val d10_3 = func3((1,2.0,"3"), '4')
    val d10_3_1: Int = d10_3(0)
    val d10_3_2: Double = d10_3(1)
    val d10_3_3: String = d10_3(2)
    val d10_3_4: Char = d10_3(3)

Output

Note that the types seem to be correct, however when I attempt to explicitly type the returned value so:

    val d10_3: *:[Int, *:[Double, *:[String, *:[Char, EmptyTuple]]]] = func3((1,2.0,"3"), '4')
    val d10_3: (Int, Double, String, Char) = func3((1,2.0,"3"), '4')

I get the following errors:

[error] -- [E007] Type Mismatch Error: /home/aaa/IdeaProjects/pdm_toyadmos/dcase2020/test/src/data/synthetic/TimeSeriesSpec.scala:617:78 
[error] 617 |    val d10_3b: *:[Int, *:[Double, *:[String, *:[Char, EmptyTuple]]]] = func3((1,2.0,"3"), '4')
[error]     |                                                                              ^^^^^^^^^^^
[error]     |                                       Found:    (Int, Double, String)
[error]     |                                       Required: Int
[error] -- [E007] Type Mismatch Error: /home/aaa/IdeaProjects/pdm_toyadmos/dcase2020/test/src/data/synthetic/TimeSeriesSpec.scala:617:91 
[error] 617 |    val d10_3b: *:[Int, *:[Double, *:[String, *:[Char, EmptyTuple]]]] = func3((1,2.0,"3"), '4')
[error]     |                                                                                           ^^^
[error]     |                      Found:    ('4' : Char)
[error]     |                      Required: Double *: String *: Char *: EmptyTuple
[error] -- [E007] Type Mismatch Error: /home/aaa/IdeaProjects/pdm_toyadmos/dcase2020/test/src/data/synthetic/TimeSeriesSpec.scala:618:52 
[error] 618 |    val d10_3c: (Int, Double, String, Char) = func3((1,2.0,"3"), '4')
[error]     |                                                    ^^^^^^^^^^^
[error]     |                                       Found:    (Int, Double, String)
[error]     |                                       Required: Int
[error] -- [E007] Type Mismatch Error: /home/aaa/IdeaProjects/pdm_toyadmos/dcase2020/test/src/data/synthetic/TimeSeriesSpec.scala:618:65 
[error] 618 |    val d10_3c: (Int, Double, String, Char) = func3((1,2.0,"3"), '4')
[error]     |                                                                 ^^^
[error]     |                                      Found:    ('4' : Char)
[error]     |                                      Required: (Double, String, Char)
[error] four errors found

Expectation

I expected to have no type errors. I have noticed that if I rename one of the functions and use the type it works. Note that func31 is an exact copy of @targetName("func3_left") but causes no issue. This is why I tried using @targetName, but it makes no difference.

While investigating this, someone tried this in the REPL and observed that the type was "unreduced". I was told that this is not the expected behavior. I am just adding this information here just in case it is of any use.

Tested with 3.0.0-M2

b-studios commented 3 years ago

Annotating the expected value, the inference appears to chose the first overload, ignoring whether it can find instances for Not and then immediately reports that A = Int != typeof (1,2.0,"3") and B = (Double, String, Char) != typeof '4'.

smarter commented 3 years ago

Annotating the expected value, the inference appears to chose the first overload, ignoring whether it can find instances for Not

This is expected behavior: as per the specification, overloading resolution is done using only the first parameter list, once an overload is chosen, the others won't be revisited even if the following parameter lists don't match:

class A {
  def foo(x: Int)(y: String): Unit = {}
  def foo(x: Any)(y: Int): Unit = {}

  foo(1)(1) // error in Scala 2 and 3 because the first overload is chosen.
}
hmf commented 3 years ago

@smarter This is strange. As noted above it works perfectly if I do not make the type explicit. Why should this change because of a "simple" type declaration? From a non-expert's point of view this seems inconsistent. I assumed type inference is "undirectional".

Additionally does this mean that using @targetName or implicit dummy parameters should not work to disambiguate the overloads (@targetName is a different case)? Short of having distinct names is there any way to circumvent this? I am hoping to support stuff like 1 && 2, 1 && (1,2,3), ... Not having overloading here is not practical.

Apologies for the questions but this "ruins" the planned API.

EDIT: I played around with the example above. If instead of the Any I use a specific well defined type then it works - no need to disambiguate. I see you used that to simulate the inference required in my example.

TIA

smarter commented 3 years ago

I assumed type inference is "undirectional".

No, it's bidirectional. But I agree that adding a type annotation shouldn't break things, I don't know what's going on here but I'm guessing it's related to match types and could be minimized further.

hmf commented 3 years ago

No, it's bidirectional.

oops, that's what I wanted to say

Ok. I am going to see if I can avoid the overloading.

could be minimized further.

I will take another look to see if I can reduce it further. If you have any suggestions, let me know.

Thank you

hmf commented 3 years ago

One more data point that may help diagnose this. The following is an example that shows that if we explicitly set the implicit parameter, then compilation also succeeds.

  @targetName("func4_right")
  def func4[A,B<:Tuple](a : A, b : B)(implicit ev: Not[A<:<Tuple]): *:[A,B] = a *: b
  @targetName("func4_left")
  def func4[A<:Tuple,B](a : A, b : B)(implicit ev: Not[B<:<Tuple]): Tuple.Concat[A,Tuple1[B]] = a ++ Tuple1(b)

  def testMinimalOverload(): Unit = {
    val p1: (Int, Int, Int) = (1,2,3)
    val p2: Int = 4
    val c2 = func4(p1, p2)
    val c4: (Int, Int, Int, Int) = func4(p1, p2)(summon[Not[p2.type <:<Tuple]]) // ok
    val c6: (Int, Int, Int, Int) = func4(p1, p2)  // fails
  }
hmf commented 3 years ago

And this is an example that works, which seems to be inconsent with the Tuple example above:

  @targetName("func5_right")
  def func5[A,B<:Int](a : A, b : B)(implicit ev: Not[A<:<Int]): A = a
  @targetName("func5_left")
  def func5[A<:Int,B](a : A, b : B)(implicit ev: Not[B<:<Int]): B = b

  def testMinimalOverload(): Unit = {
    val p2: Int = 4

    val c8:(String,Int) = func5(("s",1), p2)
    val c10:(Int,String) = func5(p2, (1,"s"))

  }