基于Scala类型的属性提取器 - 仅获取器镜头?

4
什么是从数据容器(例如case class)中提取类型的最佳方法?
例如,如果我有一个标记类型trait PID,它是一个带有标记Int的type ProductId = Int with Tagged[PID]或者Scalaz风格的type ProductId = Int @@ PID,以及产品中的其他字段type Name = String @@ PName等等,并且一个数据容器包含了产品的属性;
case class Product(pid: ProductId, name: Name, weight: Weight)

如何在不使用反射的情况下编写通用提取器 A => B 的方法?这个问题是因为我想在运行时从产品容器中动态提取一个字段。也就是说,用户传递了他们想要提取的产品属性。

比如,如果我想动态获取ProductId,我能否编写一个方法,该方法接受类型并返回值,例如:

trait Extractor[A] {
  def extract[B](i: A): B = //get type B from the Product class
}

或许我在过度复杂化问题。

我可以编写一个简单的提取器类,它接受A => B函数并为每种类型定义它;

trait Getter[A, B] {
  def extract(i: A): B
}
//... mix this in...
trait GetPID extends Getter[Product, ProductId] {
  override def extract(implicit i: Product) = i.pid
}
trait GetName extends Getter[Product, Name] {
  override def extract(implicit i: Product) = i.name
}

然后根据需要添加它们。
val dyn = new DynamicProductExtractor with GetPID 
dyn.extract

但这似乎很麻烦。

我认为像 Lens 这样的工具在这里会很有用。

1个回答

8

为了举例完整,假设我们有以下类型和一些示例数据:

import shapeless._, tag._

trait PID; trait PName; trait PWeight

type ProductId = Int @@ PID
type Name = String @@ PName
type Weight = Double @@ PWeight

case class Product(pid: ProductId, name: Name, weight: Weight)

val pid = tag[PID](13)
val name = tag[PName]("foo")
val weight = tag[PWeight](100.0)

val product = Product(pid, name, weight)

我在这里使用Shapeless的标签,但以下所有内容都适用于Scalaz或您自己的Tagged。现在假设我们想要能够在任意case类中按类型查找成员,我们可以使用Shapeless的Generic创建一个提取器:

import ops.hlist.Selector

def extract[A] = new {
  def from[C, Repr <: HList](c: C)(implicit
    gen: Generic.Aux[C, Repr],
    sel: Selector[Repr, A]
  ) = sel(gen.to(c))
}

请注意,出于简洁的原因,我使用了结构类型,但您可以非常容易地定义一个新类来完成同样的任务。
现在我们可以写如下内容:
scala> extract[ProductId].from(product)
res0: Int with shapeless.tag.Tagged[PID] = 13

请注意,如果 case class 中有多个具有请求类型的成员,则将返回第一个成员。如果它没有任何具有正确类型的成员(例如 extract[Char] from(product)),则会得到一个漂亮的编译时错误。
在这里您可以使用 lens,但是您需要编写更多或更少相同的机制 - 我不知道是否有 lens 实现可以为您提供按类型索引(例如 Shapeless 提供位置索引和按成员名称索引)。
(请注意,这实际上并不是“动态”的,因为您在 case class 中查找的类型必须在编译时静态已知,但在上述示例中也是如此。)

谢谢你的出色回答!这看起来非常整洁!就性能而言,这个怎么样?与反射相比如何? - NightWolf
这段代码在一个 case 类中也无法编译通过:scala> case class Product(pid: ProductId, name: Name, weight: Weight) error: type mismatch; found : Double required: AnyRef Note: an implicit exists from scala.Double => java.lang.Double, but methods inherited from Object are rendered ambiguous. This is to avoid a blanket implicit which would convert any scala.Double to any AnyRef. You may wish to use a type ascription: x: java.lang.Double. @TravisBrown,有什么想法吗? - NightWolf
有没有办法让它更加动态/通用,这样我就可以编写一个实现带泛型的“从中提取”功能的特质? - NightWolf
另外,我很想了解你为什么需要一个trait,但是你可以通过使用相同的AC类型参数,并要求相同的Generic.AuxSelector证据来使我的解决方案通用化。 - Travis Brown
这是使用Scala 2.10.4编译。 - NightWolf
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接