Skip to content

Inconsistencies in typechecking pattern matching for type-parameter and type-member GADT #17235

Open
@DmytroMitin

Description

@DmytroMitin

Compiler version

3.2.2

Motivation

The following code compiles for type-parameter GADTs:

  1. case classes
trait A[T]
case class B() extends A[Int]
case class C() extends A[String]

// compiles
def foo11[T](a: A[T]): T =
  a match
    case B() => 1
    case C() => "a"

// compiles
def foo12[T](a: A[T]): T =
  a match
    case _: B => 1
    case _: C => "a"
  1. objects
trait A[T]
object B extends A[Int]
object C extends A[String]

// compiles
def foo21[T](a: A[T]): T =
  a match
    case B => 1
    case C => "a"

// compiles
def foo22[T](a: A[T]): T =
  a match
    case _: B.type => 1
    case _: C.type => "a"

But none of the following compiles for type-member GADTs:

Minimized code

  1. case classes
/*sealed*/ trait A:
  type T

/*final*/ case class B() extends A:
  override type T = Int

/*final*/ case class C() extends A:
  override type T = String

// doesn't compile
def foo31(a: A): a.T =
  a match
    case B() => 1   // Found: (1 : Int),      Required: a.T
    case C() => "a" // Found: ("a" : String), Required: a.T

// doesn't compile
def foo32(a: A): a.T =
  a match
    case _: B => 1   // Found: (1 : Int),      Required: a.T
    case _: C => "a" // Found: ("a" : String), Required: a.T

// doesn't compile (but in 2.13.10 it compiles for final B, C)
def foo311[_T](a: A {type T = _T}): _T =
  a match
    case B() => 1   // Found: (1 : Int),      Required: _T
    case C() => "a" // Found: ("a" : String), Required: _T

// doesn't compile (but in 2.13.10 it compiles for final B, C)
def foo321[_T](a: A {type T = _T}): _T =
  a match
    case _: B => 1   // Found: (1 : Int),      Required: _T
    case _: C => "a" // Found: ("a" : String), Required: _T
  1. (case) objects
/*sealed*/ trait A:
  type T

/*final*/ /*case*/ object B extends A:
  override type T = Int

/*final*/ /*case*/ object C extends A:
  override type T = String

// doesn't compile
def foo41(a: A): a.T =
  a match
    case B => 1   // Found: (1 : Int),      Required: a.T
    case C => "a" // Found: ("a" : String), Required: a.T

// doesn't compile
def foo42(a: A): a.T =
  a match
    case _: B.type => 1   // Found: (1 : Int),      Required: a.T
    case _: C.type => "a" // Found: ("a" : String), Required: a.T

// doesn't compile (but in 2.13.10 it compiles)
def foo411[_T](a: A {type T = _T}): _T =
  a match
    case B => 1   // Found: (1 : Int),      Required: _T
    case C => "a" // Found: ("a" : String), Required: _T

// doesn't compile (but in 2.13.10 it compiles for final B, C)
def foo421[_T](a: A {type T = _T}): _T =
  a match
    case _: B.type => 1   // Found: (1 : Int),      Required: _T
    case _: C.type => "a" // Found: ("a" : String), Required: _T

In Scala 3 the behavior doesn't depend on whether A is sealed, B, C are final/non-final case classes/objects/case objects

Expectation

Code snippets 3-4 should compile too. Or is it intended difference between type parameters and type members? (Functional dependencies?)

Workarounds

  • match types
type Foo[X <: A] = X match
  case B => Int
  case C => String

def foo[X <: A](a: X): Foo[A] = a match
  case _: B => 1
  case _: C => "a"
  • inlining and implicit hints
inline def foo(a: A): a.T = inline a match
  case _: B => summonFrom {
    case _: (Int =:= a.T) => 1
  }
  case _: C => summonFrom {
    case _: (String =:= a.T) => "a"
  }
  • type classes

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions