scala / scala3

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

Nonunified types in GADT pattern match #15554

Closed Swoorup closed 2 years ago

Swoorup commented 2 years ago

Compiler version

3.1.3

Minimized code

enum PingMessage[Response]:
  case Ping(from: String) extends PingMessage[String]

val pongBehavior: [O] => (Unit, PingMessage[O]) => (Unit, O) = [O] =>
  (state: Unit, msg: PingMessage[O]) =>
    msg match
      case PingMessage.Ping(from) => ((), s"Pong from $from")

Scastie: https://scastie.scala-lang.org/omCKwdwSQ2uGyiYSPRbOLQ

Output

Found:    [O] => (Unit, PingMessage[O]) => (Unit, String)
Required: [O] => (Unit, PingMessage[O²]) => (Unit, O²)

where:    O  is a type variable
          O² is a type variable

Expectation

Compiles successfully

Swoorup commented 2 years ago

Appears to resolve the issue if I enforce the string type explicitly like

val pongBehavior: [O] => (Unit, PingMessage[O]) => (Unit, O) = [O] =>
  (state: Unit, msg: PingMessage[O]) =>
    msg match
      case PingMessage.Ping(from) => ((), s"Pong from $from": O)  
prolativ commented 2 years ago

A slightly smaller minimization without a custom type:

val poly: [O] => Option[O] => O = [O] =>
  (opt: Option[O]) =>
    opt match
      case Some(s: String) => (s: String)
[error] Found:    [O] => (Option[O]) => String
[error] Required: [O] => (Option[O²]) => O²
[error] 
[error] where:    O  is a type variable
[error]           O² is a type variable
[error]       case Some(s: String) => (s: String)
[error]                                          ^

The error is quite confusing here. It should be either

Required: [O] => (Option[O]) => O

or

Required: [O²] => (Option[O²]) => O²

Secondly, it seems that the problem also occurs for methods:

def poly[O]: Option[O] => O =
  (opt: Option[O]) =>
    opt match
      case Some(s: String) => (s: String)
[error] Found:    String
[error] Required: O
[error]       case Some(s: String) => (s: String)
[error]                                ^^^^^^^^^

Here the problem disappears when the type ascription gets removed

      case Some(s: String) => s
Swoorup commented 2 years ago

@prolativ expanding your example, I found another issue with inference. Should I raise a different ticket for this?

val poly: [O] => Option[O] => O =
  case Some(s: String) => s
Missing parameter type

I could not infer the type of the parameter x$1 of expanded function:
x$1 => 
  x$1 match 
    {
      case Some(s:String) => 
        s
    }.
Not found: s
prolativ commented 2 years ago

I think the compiler is right rejecting this code although the compilation error might not be very informative here. The point is that case Some(s: String) => s is a monomorphic (partial) function while [O] => Option[O] => O is a polymorphic function type. Something like

val poly: [O] => Option[O] => O =
  [O] => { case Some(s: String) => s }

might work in the future but currently it's not possible because of

Implementation restriction: polymorphic function literals must have a value parameter

Currently we have to live with

val poly: [O] => Option[O] => O =
  [O] => (o: Option[O]) => o match { case Some(s: String) => s }

I would expect

val poly: [O] => Option[O] => O =
  [O] => o => o match { case Some(s: String) => s }

to work too but this fails with

[error] cannot infer type; expected type <?> is not fully defined
[error]   [O] => o => o match { case Some(s: String) => s }
[error]          ^
[error] Not found: s
[error]   [O] => o => o match { case Some(s: String) => s }
[error]                                                 ^
[error] Found:    [O] => (<error cannot infer type; expected type <?> is not fully defined>) => 
[error]   <error Not found: s>
[error] Required: [O] => (Option[O]) => O
[error]   [O] => o => o match { case Some(s: String) => s }
[error]

which indeed looks like a bug to me

dwijnand commented 2 years ago

A slightly smaller minimization without a custom type:

val poly: [O] => Option[O] => O = [O] =>
  (opt: Option[O]) =>
    opt match
      case Some(s: String) => (s: String)

There's some funkiness that needs attention, but your minimisation is different because now the scrutinee type is co-variant. And I think the behaviour is right because O is more precise than String, so you're widening by using the type ascription, akin to expecting (s: Any) to work.

SethTisue commented 2 years ago

Dale and I have been looking at this further.

Our current hypothesis is that the root cause here is that when a polymorphic function literal is desugared, the result type of the apply method is always left blank and it's up to type inference to fill it in. Desugar.scala line 1734 creates this DefDef:

DefDef(nme.apply, applyTParams :: applyVParams :: Nil, TypeTree(), res)

where the TypeTree() is the missing result type of the apply.

Let's return to Dale's minimization. To keep this straight in your head, it's helpful not to reuse the same type parameter name. So the minimization becomes:

val poly: [O1] => Option[O1] => O1 =    // line 1
  [O2] =>                               // line 2
    (opt: Option[O2]) => ...            // line 3

line 1 is where we say what the expected type of the polymorphic function is, and that expected type includes the information that the body's result type is O1.

But when lines 2 and 3 are desugared, the desugaring includes def apply(...) =, whereas what we want is def apply(...): O2 = ..., since if we don't put O2 there, inference puts String there instead, which is wrong.

So we'd need to take the expected type [O1] => Option[O1] => O1, extract the third O1, replace all of the type parameters according to their indices so that O1 becomes O2, and then fill that in.

dwijnand commented 2 years ago

Again: keep an eye out for variance. If you switch to a covariant Option/Some instead of sticking to something invariant like PingMessage/Ping was, the behaviour is going to be different.