Skip to content

Pattern matching value s against a literal does not refine s.type to that literal #22887

Open
@TomasMikula

Description

@TomasMikula

Compiler version

3.6.3

Minimized code

def go[F[_], R](
  s: "x" | "y", 
  fs: F[s.type],
  handleX: F["x"] => R,
  handleY: F["y"] => R,
): R =
  s match
    case "x" => handleX(fs) // Error
    case "y" => handleY(fs) // Error

https://scastie.scala-lang.org/qUB4su3XSgumJBtwnyUgmQ

Output

Found:    (fs : F[(s : ("x" : String) | ("y" : String))])
Required: F[("x" : String)]

Found:    (fs : F[(s : ("x" : String) | ("y" : String))])
Required: F[("y" : String)]

Expectation

After matching value s against "x", the type F[s.type] of fs should be refined to F["x"].

Workarounds

Using a GADT instead of union type works, but that's beside the point, as it's not applicable in all situations.
enum XorY[A]:
  case X extends XorY["x"]
  case Y extends XorY["y"]

def go[S, F[_], R](
  s: XorY[S], 
  fs: F[S],
  handleX: F["x"] => R,
  handleY: F["y"] => R,
): R =
  s match
    case XorY.X => handleX(fs)
    case XorY.Y => handleY(fs)

https://scastie.scala-lang.org/tyEprpkaS7mqkg6yj2XA0A

An unsatisfactory and bloated workaround The following workaround compiles while retaining the original method signature. However, not only is it an overkill, but it also triggers a **false exhaustivity warning**.
def go[F[_], R](
  s: "x" | "y", 
  fs: F[s.type],
  handleX: F["x"] => R,
  handleY: F["y"] => R,
): R =
  s match // Warning: match may not be exhaustive. It would fail on pattern case: "x", "y"
    case x: ("x" & s.type) =>
      val ev1: x.type =:= s.type = SingletonType(x).deriveEqual(SingletonType(s))
      val ev2: x.type =:= "x"    = SingletonType(x).deriveEqual(SingletonType("x"))
      val ev:  s.type =:= "x"    = ev1.flip andThen ev2
      val fx: F["x"] = ev.substituteCo(fs)
      handleX(fx)
    case y: ("y" & s.type) =>
      val ev1: y.type =:= s.type = SingletonType(y).deriveEqual(SingletonType(s))
      val ev2: y.type =:= "y"    = SingletonType(y).deriveEqual(SingletonType("y"))
      val ev:  s.type =:= "y"    = ev1.flip andThen ev2
      val fy: F["y"] = ev.substituteCo(fs)
      handleY(fy)

sealed trait SingletonType[T] {
  val value: T
  def witness: value.type =:= T

  /** If a supertype `U` of singleton type `T` is still a singleton type,
   *  then `T` and `U` must be the same singleton type.
   */
  def deriveEqual[U >: T](that: SingletonType[U]): T =:= U =
    summon[T =:= T].asInstanceOf[T =:= U] // safe by the reasoning in the comment
}

object SingletonType {
  def apply(x: Any): SingletonType[x.type] =
    new SingletonType[x.type] {
      override val value: x.type = x
      override def witness: value.type =:= x.type = summon
    }
}

https://scastie.scala-lang.org/AhV6kSFtR4CVCNPYAgHKgg

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