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
方法使用 constValue
或 ValueOf
提取标签值。我已经能够使这些工作,(a)我需要能够在类型类实例中这样做,并且(b)我很好奇为什么我自己的方法不起作用!
为了更清楚地了解我的用例,我提出的模式类型将联产品的子类型编码为Subtype[T, ST, N <: String & 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
镜像中提取元素标签,但不能从 Sum
s 中提取元素标签。在后一种情况下,编译器似乎无法正确推断出 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】:
好的,我想出了一个解决方法!与其参数化Mirror
的MirroredElemLabels
类型以将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 中的表中提取与列表中存在的索引相对应的行。?