Skip to content

Nonunified types in GADT pattern match #15554

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

Closed
Swoorup opened this issue Jun 30, 2022 · 7 comments · Fixed by #15570
Closed

Nonunified types in GADT pattern match #15554

Swoorup opened this issue Jun 30, 2022 · 7 comments · Fixed by #15570
Assignees
Milestone

Comments

@Swoorup
Copy link

Swoorup commented Jun 30, 2022

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 Swoorup added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Jun 30, 2022
@Swoorup
Copy link
Author

Swoorup commented Jun 30, 2022

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)  

@Swoorup Swoorup changed the title GADT weirdness A polymorphic function type weirdness Jun 30, 2022
@Swoorup Swoorup changed the title A polymorphic function type weirdness Polymorphic function type weirdness Jun 30, 2022
@Swoorup Swoorup changed the title Polymorphic function type weirdness Polymorphic function type inference? weirdness Jun 30, 2022
@prolativ
Copy link
Contributor

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

@prolativ prolativ added area:gadt and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels Jun 30, 2022
@prolativ prolativ changed the title Polymorphic function type inference? weirdness Nonunified types in GADT pattern match Jun 30, 2022
@Swoorup
Copy link
Author

Swoorup commented Jun 30, 2022

@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
Copy link
Contributor

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
Copy link
Member

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.

@dwijnand dwijnand self-assigned this Jul 1, 2022
@SethTisue
Copy link
Member

SethTisue commented Jul 1, 2022

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 dwijnand linked a pull request Jul 1, 2022 that will close this issue
@dwijnand
Copy link
Member

dwijnand commented Jul 1, 2022

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.

@Kordyjan Kordyjan added this to the 3.2.1 milestone Aug 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants