scala3 icon indicating copy to clipboard operation
scala3 copied to clipboard

Match types and union patterns

Open Katrix opened this issue 3 years ago • 4 comments

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.

Katrix avatar Sep 20 '22 16:09 Katrix

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]
}

Katrix avatar Sep 20 '22 21:09 Katrix

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).

jchyb avatar Sep 21 '22 09:09 jchyb

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?

Katrix avatar Sep 21 '22 17:09 Katrix

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.

odersky avatar Sep 22 '22 15:09 odersky