使用Shapeless将嵌套的case类转换为嵌套的Map

19

我正在尝试使用Shapeless解决[这个][1]问题,总的来说就是将嵌套的case class转换为Map[String,Any],以下是示例:

case class Person(name:String, address:Address)
case class Address(street:String, zip:Int)

val p = Person("Tom", Address("Jefferson st", 10000))

目标是将p转换为以下内容:
Map("name" -> "Tom", "address" -> Map("street" -> "Jefferson st", "zip" -> 10000))

我正在尝试使用Shapeless的LabelledGeneric来实现,以下是我的进展:

import shapeless._
import record._, syntax.singleton._
import ops.record._
import shapeless.ops.record._

def writer[T,A<:HList,H<:HList](t:T)
(implicit lGeneric:LabelledGeneric.Aux[T,A],
 kys:Keys.Aux[A,H],
 vls:Values[A]) = {
    val tGen = lGeneric.to(t)
    val keys = Keys[lGeneric.Repr].apply
    val values = Values[lGeneric.Repr].apply(tGen)
    println(keys)
    println(values)
  }

我正在尝试让一个递归的写入器检查每个值并尝试为值中的每个元素创建Map。上述代码工作正常,但当我想要使用以下代码迭代values与样本Poly时,出现了以下错误。

values.map(identity)
//or
tGen.map(identity)

Error:(75, 19) could not find implicit value for parameter mapper: shapeless.ops.hlist.FlatMapper[shapeless.poly.identity.type,vls.Out]
    values.flatMap(identity)
                  ^
Error:(75, 19) not enough arguments for method flatMap: (implicit mapper: shapeless.ops.hlist.FlatMapper[shapeless.poly.identity.type,vls.Out])mapper.Out.
Unspecified value parameter mapper.
    values.flatMap(identity)
                  ^

我不知道为什么会出现那个错误。如果有更简单的方法可以使用Shapeless完成整个过程,我也很乐意了解。 [1]: Scala宏用于嵌套case类到Map和其他方式的转换

2个回答

26
任何时候,当您想要在类型未知的HList上执行像flatMap之类的操作时,您需要提供证据(以隐式参数的形式),证明该操作实际上对于该类型是可用的。这就是为什么编译器会抱怨缺少FlatMapper实例的原因——它不知道如何在没有它们的情况下在任意HList上执行flatMap(identity)
更干净的方法是定义一个自定义类型类。Shapeless已经为记录提供了ToMap类型类,并且我们可以将其作为起点,尽管它并没有提供您要查找的确切功能(它不能递归地在嵌套的case类上工作)。
我们可以编写以下内容:
import shapeless._, labelled.FieldType, record._

trait ToMapRec[L <: HList] { def apply(l: L): Map[String, Any] }

现在我们需要为三种情况提供实例。第一种情况是基本情况——空记录——由下面的hnilToMapRec处理。
第二种情况是我们知道如何转换记录的尾部,并且我们知道头部是我们也可以递归转换的东西(这里是hconsToMapRec0)。
最后一种情况类似,但是针对没有ToMapRec实例的头部(hconsToMapRec1)。请注意,我们需要使用一个LowPriority特质来确保这个实例与hconsToMapRec0相比具有正确的优先级——如果我们不这样做,这两个实例将具有相同的优先级,我们将得到有关模糊实例的错误。
trait LowPriorityToMapRec {
  implicit def hconsToMapRec1[K <: Symbol, V, T <: HList](implicit
    wit: Witness.Aux[K],
    tmrT: ToMapRec[T]
  ): ToMapRec[FieldType[K, V] :: T] = new ToMapRec[FieldType[K, V] :: T] {
    def apply(l: FieldType[K, V] :: T): Map[String, Any] =
      tmrT(l.tail) + (wit.value.name -> l.head)
  }
}

object ToMapRec extends LowPriorityToMapRec {
  implicit val hnilToMapRec: ToMapRec[HNil] = new ToMapRec[HNil] {
    def apply(l: HNil): Map[String, Any] = Map.empty
  }

  implicit def hconsToMapRec0[K <: Symbol, V, R <: HList, T <: HList](implicit
    wit: Witness.Aux[K],
    gen: LabelledGeneric.Aux[V, R],
    tmrH: ToMapRec[R],
    tmrT: ToMapRec[T]
  ): ToMapRec[FieldType[K, V] :: T] = new ToMapRec[FieldType[K, V] :: T] {
    def apply(l: FieldType[K, V] :: T): Map[String, Any] =
      tmrT(l.tail) + (wit.value.name -> tmrH(gen.to(l.head)))
  }
}

最后,我们提供一些便利的语法:
implicit class ToMapRecOps[A](val a: A) extends AnyVal {
  def toMapRec[L <: HList](implicit
    gen: LabelledGeneric.Aux[A, L],
    tmr: ToMapRec[L]
  ): Map[String, Any] = tmr(gen.to(a))
}

然后我们可以展示它是如何工作的:

scala> p.toMapRec
res0: Map[String,Any] = Map(address -> Map(zip -> 10000, street -> Jefferson st), name -> Tom)

请注意,对于嵌套的 case classes 在列表、元组等类型中的情况,此方法无法使用,但您可以轻松地将其扩展到这些情况。

感谢您提供的神奇代码!我有一个问题,如果我们从Map[String,Any]转换到case class,是否有好的方法可以做到这一点,因为我们已经失去了所有的类型信息? - Omid
1
@Omid 太好了!不过可能值得提一个新问题。 :) - Travis Brown
谢谢,如果没有人比我先完成,我会尽快尝试的。 - Travis Brown
1
@Omid,哦不,抱歉——你需要一个ToMapRec来表示A的通用表示,而不是A本身。可能值得提出一个新问题,以避免进一步拉长评论。 - Travis Brown
1
@Omid 与其返回 ToMapRec[FieldType[K, V] :: T],你可以有一个方法返回 ToMapRec[FieldType[K, List[V]] :: T] 等等。可能值得提一个新的问题。 - Travis Brown
显示剩余9条评论

3

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