Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Strange type inference for extension method with a leading using clause #11718

Closed
prolativ opened this issue Mar 12, 2021 · 6 comments
Closed

Comments

@prolativ
Copy link
Contributor

Compiler version

3.0.0-RC2-bin-20210311-cc87d06-NIGHTLY

Minimized code

trait Foo[A]

extension [A](using fa: Foo[A])(a: A)
  def foo[B](b: Boolean) = ???

// given Foo[String] with {}
// given Foo[Int] with {}
// given Foo[Boolean] with {}

val x = 1.foo(true)

Output

Depending on the given instances of Foo[A] we uncomment the compilation results are as follows:

  1. No instances
10 |val x = 1.foo(true)
   |        ^^^^^
   |value foo is not a member of Int.
   |An extension method was tried, but could not be fully constructed:
   |
   |    foo[A](/* missing */summon[Foo[?]])(1)    failed with
   |
   |        no implicit argument of type Foo[A] was found for parameter fa of method foo

Output more or less as one might expect except that there are [Foo[?]] and Foo[A] instead of Foo[Int]

  1. Instance only for Int - compiles successfully

  2. Instance only for Boolean:

10 |val x = 1.foo(true)
   |        ^^^^^
   |    value foo is not a member of Int.
   |    An extension method was tried, but could not be fully constructed:
   |
   |        foo[Boolean](Tmp$package.given_Foo_Boolean)(1)[B]    failed with
   |
   |            Found:    (1 : Int)
   |            Required: Boolean

The error makes sense assuming that the compiler first tries to find the implicit, finds the only existing instance and uses it to instantiate the type variable. Although it would be nice if we could infer the type correctly here.

  1. Instance only for String:
10 |val x = 1.foo(true)
   |        ^^^^^
   |      value foo is not a member of Int.
   |      An extension method was tried, but could not be fully constructed:
   |
   |          foo[String](Tmp$package.given_Foo_String)(1)[B]    failed with
   |
   |              Found:    (1 : Int)
   |              Required: String

Result analogous to case 3

  1. Instances only for String and Boolean:
10 |val x = 1.foo(true)
   |        ^^^^^
   |    value foo is not a member of Int.
   |    An extension method was tried, but could not be fully constructed:
   |
   |        foo[Boolean](Tmp$package.given_Foo_Boolean)(1)[B]    failed with
   |
   |            Found:    (1 : Int)
   |            Required: Boolean

This obviously should not compile because there's no instance for Int. Alternatively the compiler could complain that there are 2 given instances and it doesn't know which one to pick. This would be somehow consistent with cases 3 and 4. But here instead for some reason the type parameter A gets inferred as Boolean, which seems to be taken from the type of the parameter b.

  1. Instances only for String and Int:
10 |val x = 1.foo(true)
   |        ^^^^^
   |value foo is not a member of Int.
   |An extension method was tried, but could not be fully constructed:
   |
   |    foo[A](/* missing */summon[Foo[? >: (true : Boolean)]])(1)    failed with
   |
   |        no implicit argument of type Foo[A] was found for parameter fa of method foo
   |        
   |        Note: given instance object given_Foo_String was not considered because it was not imported with `import given`.

Now the compiler tries to instantiate A as a supertype of Boolean (which again doesn't make sense) instead of successfully choosing the instance for Int or complaining about the ambiguity. Also note that the Note message doesn't seem to make sense as given_Foo_String is declared in the current scope.

  1. Instances only for Int and Boolean
10 |val x = 1.foo(true)
   |        ^^^^^
   |    value foo is not a member of Int.
   |    An extension method was tried, but could not be fully constructed:
   |
   |        foo[Boolean](Tmp$package.given_Foo_Boolean)(1)[B]    failed with
   |
   |            Found:    (1 : Int)
   |            Required: Boolean

Same as case 5 but here the instance for Int could in theory be chosen to make this compile.

  1. All instances - same as 7

Additionally for all cases with more than one instance (5-8) we do indeed get an ambiguous implicit error like this

10 |val x = 1.foo[Any](true)
   |        ^^^^^
   |value foo is not a member of Int.
   |An extension method was tried, but could not be fully constructed:
   |
   |    foo[A](
   |      /* ambiguous: both object given_Foo_String and object given_Foo_Int match type Foo[A] */
   |        summon[Foo[A]]
   |    )(1)    failed with
   |
   |        ambiguous implicit arguments: both object given_Foo_String and object given_Foo_Int match type Foo[A] of parameter fa of method foo

if we pass a dummy type argument to the invocation of foo like val x = 1.foo[Any](true).

Also if we try to invoke the extension method foo as a normal method (val y = foo(1)(true)) this doesn't work either unless there's only an instance for Int.

Expectation

There is a simple workaround to make this work as expected by putting the first using section after the receiver parameter like this

extension [A](a: A)(using fa: Foo[A])

but this seems to only hide the problem instead of actually resolving it.
In general we could either try to make the code example compile as long as there is an available given instance for Int (even if there are also instances for other types) or at least uniformly show correct error messages (the message about ambiguous implicits might be good enough but a message about not being able to infer the type parameter based on arguments from a parameters list which does not directly follow the type parameters list would be even better)

@prolativ
Copy link
Contributor Author

Actually this seems to be a problem also if we define foo as an ordinary method

def foo[A](using fa: Foo[A])(a: A)(b: Boolean) = ???

Here we can't put the second type parameters section but this doesn't seem to matter.

@odersky
Copy link
Contributor

odersky commented Mar 14, 2021

I don't get how this is a bug. You describe the behavior of the type inferencer. In what sense is this wrong?

I believe for situations like this, it's better to leave things as they are unless you have a concrete, implemented change proposal that we can discuss. Otherwise this reduces the bandwidth of maintainers who are already overloaded.

@prolativ
Copy link
Contributor Author

In general A should not be inferred as Boolean here. If it's not too difficult to implement, the inferencer should be able to infer it as Int, otherwise the compiler error should at least return an error message which correctly explains the problem (which would be ambiguity of implicits). Probably the problem gets easier to notice in case of an ordinary method rather than an extension:

trait Foo[A]

def foo[A](using fa: Foo[A])(a: A)(b: Boolean) = ???

given Foo[Int] with {}
given Foo[Boolean] with {}

val y = foo(1)(true)

returns the error

8 |val y = foo(1)(true)
  |            ^
  |            Found:    (1 : Int)
  |            Required: Boolean

while there's no reason the instance for Boolean should be in any way preferred to the instance for Int.

trait Foo[A]

def foo[A](using fa: Foo[A])(a: A)(b: Boolean) = ???

given Foo[Int] with {}
given Foo[String] with {}

val y = foo(1)(true)
8 |val y = foo(1)(true)
  |           ^
  |no implicit argument of type Foo[A] was found for parameter fa of method foo
  |
  |Note: given instance object given_Foo_Int was not considered because it was not imported with `import given`.

Here it's obviously not true that there is no instance of Foo[A].

Generally it looks as if unless there's there's exactly one given instance matching the type of the first explicit parameter given Foo[Int] the compiler for an unknown reason tries to unify A with the type of the second explicit parameter (which is Boolean in this case)

@odersky
Copy link
Contributor

odersky commented Mar 15, 2021

I still see nothing to act on. You need to be a lot more specific what particular step of the inferencer should be changed, or you need to produce a single program that you think is mis-inferred with a good explanation how to do better.

@odersky odersky closed this as completed Mar 15, 2021
@rjolly
Copy link
Contributor

rjolly commented Mar 19, 2021

See also #11713

@prolativ
Copy link
Contributor Author

@rjolly actually I found this issue while playing around with the one you had reported but the reason of this problem seems to be different as here using non-extension methods doesn't work either

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants