This comes down to being able to eliminate types from a union type, which I don’t think is directly possible. The same problem exists in Scala 2 for intersection types (A with B
). I’ve had the need before to be able to eliminate types from an intersection type (in Scala 2) and solved it with a typeclass plus a fundep materialization macro. Here’s something similar in Scala 3 (lots of nice warnings, mainly because I had to do some backflips to construct the final union type as there doesn’t seem to be a way to construct types directly in the Scala 3 macro API yet):
trait Eliminate[A, B] {
type Out
}
object Eliminate {
import scala.quoted.{_, given}
inline given derive[A, B] <: Eliminate[A, B] = ${ deriveImpl('[A], '[B]) }
def deriveImpl[A, B](A: Type[A], B: Type[B])(given ctx: QuoteContext): Expr[Eliminate[A, B]] = {
import ctx.tasty.{_, given}
def expandOrs(typ: ctx.tasty.Type): List[ctx.tasty.Type] = typ match {
case OrType(t1, t2) => expandOrs(t1) ++ expandOrs(t2)
case typ => List(typ)
}
def orAll(typs: List[ctx.tasty.Type]): ctx.tasty.Type = {
type X1
type X2
typs match {
case last :: Nil => last
case first :: rest =>
first.seal match {
case first: scala.quoted.Type[X1] => orAll(rest).seal match {
case rest: scala.quoted.Type[X2] => or[X1, X2](given first, rest, ctx).unseal.tpe
}
}
}
}
val as = expandOrs(A.unseal.tpe)
val b = B.unseal.tpe
val withoutB = as.filterNot {
typ => typ =:= b
}
val resultOrType = orAll(withoutB).seal
'{ null: Eliminate[$A, $B] { type Out = $resultOrType }}
}
def or[X1, X2](given x1: Type[X1], x2: Type[X2], ctx: QuoteContext): Type[X1 | X2] = '[$x1 | $x2]
}
You can see it work like so:
scala> summon[(String | Int | Boolean) Eliminate Int]
val res0:
Eliminate[String | Int | Boolean, Int]{
Out = String | Boolean
} = null
(notice the Out
type has the resulting union type).
Hopefully someone who knows dotty a little more deeply can point out the proper way to do these things (like constructing a union type from two quoted types or tasty types)