scala3
scala3 copied to clipboard
Match types and union patterns
Compiler version
3.2.0
Minimized code
Not that minimized sadly, but I have very little idea what's going on. I'd give an implementation for HKDProductGeneric instead of just ???, but simplest way to do that is to not have ProductK, and instead have the RHS of ProductK appear raw. This however seems to sometimes (not now for some reason) crash the compiler.
object WrongBytecode {
import scala.compiletime._
import scala.deriving.Mirror
type Id[A] = A
type Const[A] = [B] =>> A
opaque type Finite[N <: Int] = Int
opaque type ProductK[F[_], T <: Tuple] = Tuple.Map[T, F]
type TupleUnionLub[T <: Tuple, Lub, Acc <: Lub] <: Lub = T match {
case (h & Lub) *: t => TupleUnionLub[t, Lub, Acc | h]
case EmptyTuple => Acc
}
sealed trait HKDProductGeneric[A]:
type Gen[_[_]]
type Index[A]
type ElemTop
class IdxWrapper[X](val idx: Index[X])
given [X]: Conversion[IdxWrapper[X], Index[X]] = _.idx
given [X]: Conversion[Index[X], IdxWrapper[X]] = new IdxWrapper(_)
inline def upcastIndex[X](idx: Index[X]): IdxWrapper[_ <: ElemTop] =
new IdxWrapper(idx).asInstanceOf[IdxWrapper[_ <: ElemTop]]
type Names <: String
def names: Gen[Const[Names]]
def stringToName(s: String): Option[Names]
type FieldOf[Name <: Names] <: ElemTop
def nameToIndex[Name <: Names](name: Name): Index[FieldOf[Name]]
def to(a: A): Gen[Id]
def from(gen: Gen[Id]): A
def indexK[A[_], Z](gen: Gen[A])(index: Index[Z]): A[Z]
object HKDProductGeneric:
type FieldOfImpl[Name, ElemTop, ElemTypes, Labels] <: ElemTop = (ElemTypes, Labels) match {
case (th *: tt, Name *: lt) => th & ElemTop
case (_ *: tt, _ *: lt) => FieldOfImpl[Name, ElemTop, tt, lt]
}
transparent inline given derived[A](using m: Mirror.ProductOf[A]): HKDProductGeneric[A] =
type Names = TupleUnionLub[m.MirroredElemLabels, String, Nothing]
derivedImpl[A, m.MirroredElemTypes, Names]
def derivedImpl[A, ElemTypes <: Tuple, NamesUnion <: String](
using m: Mirror.ProductOf[A] { type MirroredElemTypes = ElemTypes }
): HKDProductGeneric[A] {
type Gen[F[_]] = ProductK[F, m.MirroredElemTypes]
type Index[_] = Finite[Tuple.Size[m.MirroredElemTypes]]
type Names = NamesUnion
type ElemTop = Tuple.Union[ElemTypes]
type FieldOf[Name <: Names] = FieldOfImpl[Name, ElemTop, ElemTypes, m.MirroredElemLabels]
} = ???
end HKDProductGeneric
case class Foo(a: Int, b: String, c: Double)
object Foo {
val value1: Foo = Foo(5, "foo", 3.14)
val names: List[String] = List("a", "b", "c")
}
val instance = summon[HKDProductGeneric[Foo]]
extension [A](xs: List[A])
def traverseOption[B](f: A => Option[B]): Option[List[B]] =
val tmpRes = xs.map(f)
if tmpRes.contains(None) then None else Some(tmpRes.map(_.get))
@main
def run: Unit = {
summon[instance.Names =:= ("a" | "b" | "c")]
summon[instance.FieldOf["a"] =:= Int]
summon[instance.FieldOf["b"] =:= String]
summon[instance.FieldOf["c"] =:= Double]
val value = instance.to(Foo.value1)
val fromNamesValues = Foo.names.traverseOption((nameStr: String) =>
instance
.stringToName(nameStr)
.map { (name: instance.Names) =>
val idx: instance.Index[instance.FieldOf[name.type]] = instance.nameToIndex(name)
val res: instance.FieldOf[name.type] = instance.indexK(value)(idx)
res
}
)
}
}
Output
Inspecting the generated code decompiled by CFR, I get this.
public void run() {
$less$colon$less$.MODULE$.refl();
$less$colon$less$.MODULE$.refl();
$less$colon$less$.MODULE$.refl();
$less$colon$less$.MODULE$.refl();
Product value = (Product)this.instance().to((Object)WrongBytecode.Foo$.MODULE$.value1());
Option fromNamesValues = this.traverseOption(WrongBytecode.Foo$.MODULE$.names(), (Function1 & Serializable)nameStr -> this.instance().stringToName(nameStr).map((Function1 & Serializable)name -> {
int idx = BoxesRunTime.unboxToInt((Object)this.instance().nameToIndex(name));
Object res = this.instance().indexK((Object)value, (Object)BoxesRunTime.boxToInteger((int)idx));
return BoxesRunTime.unboxToInt((Object)res);
}));
}
That cast at the end depends completely on what the type of the first field in the case class is. As seen, the FieldOf type works just fine when applied to concrete types.
Expectation
I'd expect no cast at the end, as is gotten if I instead say map[Any] for the inner map.
Ok, think I managed to narrow it down quite a bit.
I would guess that type matches worked something like this, just like match expressions
val Name = ???
val res = tuple match {
case Name *: _ => ... //name == tuple.head is always true
}
type Name = ??? //Something
type Res = Tuple match {
case Name *: _ => ... //Name =:= Tuple.Head[Tuple] is always true
}
However, it seems that union types throw a wrench in that, so you get stuff like this.
type In[T <: Tuple, Elem] <: Boolean = T match {
case EmptyTuple => false
case Elem *: _ => true
case _ *: t => In[t, Elem]
}
summon[In[(Int, String, Boolean), Int] =:= true]
summon[In[(Int, String, Boolean), Char | Int] =:= true]
Is this intended. If so, is there any way to actually get a true equal match? So far I've just written something like this, but it is sort of tedious, and it doesn't properly reduce if the answer is false.
type Eq[A, B] <: Boolean = (A, B) match {
case (B, A) => true
case _ => false
}
type In[T <: Tuple, Elem] <: Boolean = (T, T) match {
case EmptyTuple => false
case (Elem *: _, h *: _) => Eq[Elem, h]
case _ *: t => In[t, Elem]
}
This behavior is intended and correct, as explained in the documentation. The compiler reduces the pattern right after checking if (Int, String, Boolean) <:< (Char | Int) *: _, which is true (as opposed to (Int, String, Boolean) =:= (Char | Int) *: _, which would be false).
Are there then any tools to do a =:= check and get back a boolean value? Specifically one that works both in the true and false case?
Are there then any tools to do a =:= check and get back a boolean value? Specifically one that works both in the true and false case?
You could arrange the operands as arguments of an invariant constructor and match on that, maybe.