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

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

Open
TomasMikula opened this issue Mar 28, 2025 · 0 comments

Comments

@TomasMikula
Copy link
Contributor

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

@TomasMikula TomasMikula added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Mar 28, 2025
@Gedochao Gedochao added area:pattern-matching and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels Apr 1, 2025
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

2 participants