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

Posted

技术标签:

【中文标题】Scala 3:如何从 Mirror.Sum 中提取元素的名称作为元组?【英文标题】:Scala 3: How do you extract the names of elements from a Mirror.Sum as a tuple? 【发布时间】:2022-01-21 05:53:32 【问题描述】:

我正在尝试创建一种模式类型,它可以让您以通用的、完全类型化的方式描述 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")]

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

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)我需要能够在类型类实例中这样做,并且(b)我很好奇为什么我自己的方法不起作用!

为了更清楚地了解我的用例,我提出的模式类型将联产品的子类型编码为Subtype[T, ST, N &lt;: String &amp; Singleton, S] 的元组,其中T 是超类型的类型,ST 是子类型的类型, N 是子类型名称的窄类型,S 是子类型自身架构的窄类型。我希望能够使用given 类型类实例派生这个元组。

更新 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))
         

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

【问题讨论】:

您必须在使用inline 宏派生类型类时执行此操作。这意味着您不能创建类型类转到值级别,并要求编译时反射。相反,您可以设计您的类型类,以便在构造它时嵌入有关其总和/乘积元素的知识(使用inline),然后在值级别上您只需调用类型类的实例。有关示例,请参见此处github.com/MateuszKubuszok/dbg/blob/… 换句话说,如果您希望能够使用编译时反射和/或重新设计 NamesDeriver,则必须将您的 def getNames 重写为 inline 以便它在编译时构造值时间,然后在这些新实例中返回常量。 我认为这不可能与constValue 有关。 constValue["a"] 有效,但 val a = "a"; constValue[a.type] 无效,这让我相信 any 类型的标识符不会被编译器视为常量类型。我觉得通过这些类型访问命名信息似乎是为了宏。 @MichaelZajac 我肯定在网上看到过使用constValue 从镜像中获取元素标签的工作示例,所以我很确定它可以工作。见,例如blog.oyanglul.us/scala/dotty/en/generic-type-class-derivation 我认为有一个错误,因为我已经能够从 Product 镜像中提取元素标签,但不能从 Sums 中提取元素标签。在后一种情况下,编译器似乎无法正确推断出 ElemLabels 参数。我在这里报告了这个问题:github.com/lampepfl/dotty/issues/14150. 【参考方案1】:

当我需要这个功能时,我只是编写了一个实用程序来实现这一点,它使用 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
      

【讨论】:

感谢马特乌斯!事实上,我已经能够使用这种方法检索元素标签,但是我无法在类型类中这样做,这样我就可以使用它们为每个子类型构造一个模式元组。当我尝试将using vo: ValueOf[ N ]inline given 实例一起使用时,它无法找到隐式ValueOf!所以这似乎与我在constValue 遇到的问题相同 再次感谢@MateuszKubuszok!我现在遇到了 another 问题,即当我以这种方式构造派生程序时,它似乎无法找到具有非空元素标签的 Mirror 实例。有什么想法吗?...【参考方案2】:

好的,我想出了一个解决方法!与其参数化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$4@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。

【讨论】:

以上是关于Scala 3:如何从 Mirror.Sum 中提取元素的名称作为元组?的主要内容,如果未能解决你的问题,请参考以下文章

如何从 Scala 中的映射键中获取值的常见元素?

如何在 Scala 3 枚举上进行模式匹配

如何从 SCALA 中的表中提取与列表中存在的索引相对应的行。?

如何从vert.x scala jdbc客户端检索数据库元数据?

如何从不同的数据框中添加一列:Scala Frame

如何使用 Spark scala 从字符串格式的复杂 JSON 创建数据帧