scalascala-3dottymatch-types

Scala3 type matching with multiple types


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.


Solution

  • 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:

    1. Single inheritance of classes
    2. Final classes cannot be extended
    3. Constant types with distinct values are nonintersecting
    4. 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.