Well, it's not surprising that the order of patterns in pattern matching is important. So it is in match types.
Match types work well on type level. On value level there many limitations
Scala 3: typed tuple zipping
Type-level filtering tuple in Scala 3
How to get match type to work correctly in Scala 3
Scala 3. Implementing Dependent Function Type
How do you deal with tuples of higher-order types?
How to prove that `Tuple.Map[H *: T, F] =:= (F[H] *: Tuple.Map[T, F])` in Scala 3
Tuples in Scala 3 Compiler Operations for Typeclass Derivation
scala 3 map tuple to futures of tuple types and back
Express function of arbitrary arity in vanilla Scala 3
Shapeless3 and annotations
In Scala 3, how to replace General Type Projection that has been dropped?
What does Dotty offer to replace type projections?
Sometimes match types are not so good as input types
def doSmth[C <: Context](data: Input[C]): C = data match
case _: AData => new AContext {} // doesn't compile
case _: BData => new BContext {} // doesn't compile
but ok as output types
type InverseInput[D <: Data[?]] = D match
case AData => AContext
case BData => BContext
def doSmth[D <: Data[?]](data: D): InverseInput[D] = data match
case _: AData => new AContext {}
case _: BData => new BContext {}
Sometimes type classes are better than match types
trait Doer[C <: Context]:
type Input <: Data[C]
def doThing(data: Input): Unit
object Doer:
given Doer[AContext] with
override type Input = AData
override def doThing(data: AData): Unit = println(data.name)
given Doer[BContext] with
override type Input = BData
override def doThing(data: BData): Unit = println(s"age: ${data.age}")
def doThing[C <: Context, I <: Data[C]](data: I)(using
doer: Doer[C] {type Input = I}
): Unit = doer.doThing(data)
doThing(AData("steve")) // steve
doThing(BData(40)) // age: 40
In your specific use case, you can make AContext
, BContext
objects
object AContext extends Context
object BContext extends Context
type AContext = AContext.type // for convenience, to write AContext rather than AContext.type
type BContext = BContext.type // for convenience, to write BContext rather than BContext.type
or classes
class AContext extends Context
class BContext extends Context
and your code will compile.
The thing is in reduction rules
https://docs.scala-lang.org/scala3/reference/new-types/match-types.html#match-type-reduction
Match type reduction follows the semantics of match expressions, that is, a match type of the form S match { P1 => T1 ... Pn => Tn }
reduces to Ti
if and only if s: S match { _: P1 => T1 ... _: Pn => Tn }
evaluates to a value of type Ti
for all s: S
.
The compiler implements the following reduction algorithm:
- If the scrutinee type
S
is an empty set of values (such as Nothing
or String & Int
), do not reduce.
- Sequentially consider each pattern
Pi
- If
S <: Pi
reduce to Ti
.
- Otherwise, try constructing a proof that
S
and Pi
are disjoint, or, in other words, that no value s
of type S
is also of type Pi
.
- If such proof is found, proceed to the next case (
Pi+1
), otherwise, do not reduce.
Disjointness proofs rely on the following properties of Scala types:
- Single inheritance of classes
- Final classes cannot be extended
- Constant types with distinct values are nonintersecting
- Singleton paths to distinct values are nonintersecting, such as object definitions or singleton enum cases.
When AContext
, BContext
are just traits, then they are not disjoint and the compiler doesn't proceed to the next case.