Open
Description
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)
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
}
}