Kieran
Kieran

Reputation: 406

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.

Upvotes: 2

Views: 413

Answers (1)

Dmytro Mitin
Dmytro Mitin

Reputation: 51658

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.

Upvotes: 4

Related Questions