Scala 3:如何从 Mirror.Sum 中提取元素名称作为元组?

Scala 3: How do you extract the names of elements from a Mirror.Sum as a tuple?

我正在尝试创建一个模式类型,它可以让您以通用的、完全类型化的方式描述 Scala 类型。我有它的积和联积版本,现在我正尝试使用 Scala 3 的镜像来导出它们。

我目前面临的特殊挑战是从 Mirror. 中的 MirroredElemLabels 类型中提取元素名称 我的理解是这些类型是单例类型,可以转换为它们的单例值使用 scala.compiletime.constValue.

我可以确认 MirroredElemLabels 是我在以下测试用例中所期望的:

sealed trait SuperT
final case class SubT1( int : Int ) extends SuperT
final case class SubT2( str : String ) extends SuperT

val mirror = summon[Mirror.SumOf[SuperT]]
summon[mirror.MirroredElemLabels =:= ("SubT1", "SubT2")]

我应该能够提取具有以下类型的值 class:

import scala.deriving.Mirror
import scala.compiletime.constValue

trait NamesDeriver[ T ] {
    type Names <: Tuple

    def derive : Names
}

object NamesDeriver {
    type Aux[ T, Ns ] = NamesDeriver[ T ] { type Names = Ns }

    inline given mirDeriver[ T, ELs <: Tuple ](
        using
        mir : Mirror.Of[ T ] { type MirroredElemLabels = ELs },
        der : NamesDeriver[ ELs ],
    ) : NamesDeriver[ T ] with {
        type Names = der.Names

        def derive : der.Names = der.derive
    }

    given emptyDeriver : NamesDeriver[ EmptyTuple ] with {
        type Names = EmptyTuple
        def derive : EmptyTuple = EmptyTuple
    }

    inline given labelsDeriver[ N <: String & Singleton, Tail <: Tuple ](
        using
        next : NamesDeriver.Aux[ Tail, Tail ],
    ) : NamesDeriver[ N *: Tail ] with {
        type Names = N *: Tail

        def derive : N *: Tail = constValue[ N ] *: next.derive
    }

    def getNames[ T ](
        using
        nd : NamesDeriver[ T ],
    ) : nd.Names = nd.derive
}

但这不会编译:

not a constant type: labelsDeriver.this.N; cannot take constValue

为什么我不能在这里使用 constValue

更新

我看到了几种方法,包括下面的 Mateusz Kubuszok 的方法,它们使用 inline 方法使用 constValueValueOf 提取标签值。我已经能够完成这些工作,(a) 我需要能够在类型 class 实例中这样做,并且 (b) 我很好奇为什么我自己的方法不起作用!

为了更清楚地说明我的用例,我提出的模式类型将副产品的子类型编码为 Subtype[T, ST, N <: String & Singleton, S] 的元组,其中 T 是超类型的类型,ST 是子类型的类型,N 是子类型名称的窄类型,S 是子类型自己模式的窄类型。我希望能够使用 given 类型 class 实例派生此元组。

更新 2

感谢 Mateusz 的建议,我已经能够得到以下版本来编译...

import scala.deriving.Mirror
import scala.util.NotGiven
import scala.compiletime.{constValue, erasedValue, summonAll, summonInline}

trait Deriver {
    type Derived

    def derive : Derived
}

trait MirrorNamesDeriver[ T ] extends Deriver { type Derived <: Tuple }

object MirrorNamesDeriver {
    type Aux[ T, Ns <: Tuple ] = MirrorNamesDeriver[ T ] {type Derived = Ns}

    //    def values(t: Tuple): Tuple = t match
    //        case (h: ValueOf[_]) *: t1 => h.value *: values(t1)
    //        case EmptyTuple => EmptyTuple

    inline given mirDeriver[ T, ElemLabels <: Tuple, NDRes <: Tuple ](
        using
        mir: Mirror.SumOf[ T ] {type MirroredElemLabels = ElemLabels},
        nd: NamesDeriver.Aux[ ElemLabels, ElemLabels ],
    ): MirrorNamesDeriver.Aux[ T, ElemLabels ] = {
        new MirrorNamesDeriver[ T ] {
            type Derived = ElemLabels

            def derive: ElemLabels = nd.derive
        }
    }
}

trait NamesDeriver[ R ] extends Deriver

object NamesDeriver {
    type Aux[ R, D ] = NamesDeriver[ R ] { type Derived = D }

    inline given emptyDeriver : NamesDeriver[ EmptyTuple ] with {
        type Derived = EmptyTuple
        def derive : EmptyTuple = EmptyTuple
    }

    inline given labelsDeriver[ N <: (String & Singleton), Tail <: Tuple ](
        using
        next : NamesDeriver.Aux[ Tail, Tail ],
    ) : NamesDeriver.Aux[ N *: Tail, N *: Tail ] =  {
        val derivedValue = constValue[ N ] *: next.derive

        new NamesDeriver[ N *: Tail ] {
            type Derived = N *: Tail

            def derive : N *: Tail = derivedValue
        }

    }

    inline def getNames[ T ](
        using
        nd : MirrorNamesDeriver[ T ],
    ) : nd.Derived = nd.derive

}

但是,上面的测试用例失败了:

 sealed trait SuperT
 final case class SubT1( int : Int ) extends SuperT
 final case class SubT2( str : String ) extends SuperT

 "NamesDeriver" should "derive names from a coproduct" in {
     val nms = NamesDeriver.getNames[ SuperT ]
     nms.size shouldBe 2
 }

如果我将以下证据添加到 mirDeriver 中的 using 参数列表:ev : NotGiven[ ElemLabels =:= EmptyTuple ],我会得到以下编译错误:

But no implicit values were found that match type util.NotGiven[? <: Tuple =:= EmptyTuple].

这表明 Mirror 具有 MirroredElemLabels 的空元组。但同样,对于同一个测试用例,我能够确认我可以召唤一个 MirroredElemLabels 类型为 ("SubT1", "SubtT2") 的镜像。不仅如此,在 same 编译错误中,表示没有这样的 NotGiven 实例,它报告给定的 Mirror 实例:

         {
            MirroredElemTypes = (NamesDeriverTest.this.SubT1, 
              NamesDeriverTest.this.SubT2
            ); MirroredElemLabels = (("SubT1" : String), ("SubT2" : String))
         }

这是怎么回事??剧情变厚了...

当我需要此功能时,我只是编写了一个实用程序来实现此目的,它使用 ValueOf(这类似于来自 Shapeless 的 Witness,但内置):

// T is m.MirroredElemLabels - tuple of singleton types describing labels
inline def summonLabels[T <: Tuple]: List[String] =
  inline erasedValue[T] match
    case _: EmptyTuple => Nil
    case _: (t *: ts)  => summonInline[ValueOf[t]].value.asInstanceOf[String] :: summonLabels[ts]
val labels  = summonLabels[p.MirroredElemLabels]

但您可能可以使用类似

的代码以更少的代码实现它
// 1. turn type (A, B, ...) into type (ValueOf[A], ValueOf[B], ...)
//    (for MirroredElemLabels A, B, ... =:= String) 
// 2. for type (ValueOf[A], ValueOf[B], ...) summon List[ValueOf[A | B | ...]]
//    (which should be a List[ValueOf[String]] but if Scala
//     gets confused about this you can try `.asInstanceOf`)
// 3. turn it into a List[String]
summonAll[Tuple.Map[p.MirroredElemLabels, ValueOf]]
  .map(valueOf => valueOf.value.asInstanceOf[String])

编辑:

尝试将您的代码重写为

    inline given labelsDeriver[ N <: String & Singleton, Tail <: Tuple ](
        using
        next : NamesDeriver.Aux[ Tail, Tail ],
    ) : NamesDeriver[ N *: Tail ] =
      // makes sure value is computed before instance is constructed
      val precomputed = constValue[ N ] *: next.derive
      new NamesDeriver[ N *: Tail ] {
        type Names = N *: Tail

        // apparently, compiler thinks that you wanted to put
        // constValue resolution into new instance's method body
        // rather than within macro, which is why it fails
        // so try to force it to compute it in compile-time
        def derive : N *: Tail = precomputed
      }

好的,我找到了解决方法!与其参数化 MirrorMirroredElemLabels 类型以将 NamesDeriver 作为第二个上下文参数包含在 mirDeriver 中,我们可以只使用 summonInline 来联想内联给定定义中的 NamesDeriver

    transparent inline given mirDeriver[ T ](
        using
        mir: Mirror.SumOf[ T ],
    ): MirrorNamesDeriver.Aux[ T, mir.MirroredElemLabels ] = {
        val namesDeriver = summonInline[ NamesDeriver.Aux[ mir.MirroredElemLabels, mir.MirroredElemLabels ] ]

        new MirrorNamesDeriver[ T ] {
            type Derived = mir.MirroredElemLabels

            def derive: mir.MirroredElemLabels = namesDeriver.derive
        }
    }

添加 transparent 有助于我的 IDE 识别结果类型,但它似乎与编译无关。这是测试用例的结果:

val deriver = summon[MirrorNamesDeriver[ SuperT ]]
summon[deriver.Derived =:= ("SubT1", "SubT2")]
val nms = MirrorNamesDeriver.getNames[ SuperT ]
println(nms.size)

...输出:

val deriver: MirrorNamesDeriver[SuperT]{Derived = ("SubT1", "SubT2")} = anon@79d56038
val res0: ("SubT1", "SubT2") =:= ("SubT1", "SubT2") = generalized constraint
val nms: ("SubT1", "SubT2") = (SubT1,SubT2)
2

更新

事实证明,只需使用通过上下文参数调用的类型 类 就可以做到这一点。参见 https://github.com/lampepfl/dotty/issues/14150#issuecomment-998586254