I am trying to use a Scala3 type match to achieve something similar to type projections on abstract types in Scala2.
A minimal example is:
sealed trait Context
trait AContext extends Context
trait BContext extends Context
trait Data[C <: Context]
case class AData(name: String) extends Data[AContext]
case class BData(age: Int) extends Data[BContext]
type Input[C <: Context] = C match {
case AContext => AData
case BContext => BData
}
trait Doer[C <: Context]:
def doThing(data: Input[C]): Unit
class ADoer extends Doer[AContext]:
override def doThing(data: AData): Unit = println(data.name)
class BDoer extends Doer[BContext]:
override def doThing(data: BData): Unit = println(s"age: ${data.age}")
ADoer().doThing(AData("steve"))
BDoer().doThing(BData(40))
For some reason, the order of the statements in the type match clause are important. In this case ADoer
passes the compiler and BDoer
fails, stating the doThing method does not match the type signature. If the type match clauses puts case B
first, then BDoer succeeds and ADoer fails.
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
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
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 toTi
if and only ifs: S match { _: P1 => T1 ... _: Pn => Tn }
evaluates to a value of typeTi
for alls: S
.The compiler implements the following reduction algorithm:
- If the scrutinee type
S
is an empty set of values (such asNothing
orString & Int
), do not reduce.- Sequentially consider each pattern
Pi
- If
S <: Pi
reduce toTi
.- Otherwise, try constructing a proof that
S
andPi
are disjoint, or, in other words, that no values
of typeS
is also of typePi
.- 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.