11

我目前正在实现一个库来对 XML-RPC 消息进行序列化和反序列化。它几乎完成了,但现在我正在尝试使用Shapeless删除我当前asProduct方法的样板。我当前的代码:

trait Serializer[T] {
  def serialize(value: T): NodeSeq
} 

trait Deserializer[T] {
  type Deserialized[T] = Validation[AnyErrors, T]
  type AnyErrors = NonEmptyList[AnyError]
  def deserialize(from: NodeSeq): Deserialized[T]
}

trait Datatype[T] extends Serializer[T] with Deserializer[T]

// Example of asProduct, there are 20 more methods like this, from arity 1 to 22
def asProduct2[S, T1: Datatype, T2: Datatype](apply: (T1, T2) => S)(unapply: S => Product2[T1, T2]) = new Datatype[S] {
  override def serialize(value: S): NodeSeq = {
    val params = unapply(value)
    val b = toXmlrpc(params._1) ++ toXmlrpc(params._2)
    b.theSeq
  }

  // Using scalaz
  override def deserialize(from: NodeSeq): Deserialized[S] = (
      fromXmlrpc[T1](from(0)) |@| fromXmlrpc[T2](from(1))
    ) {apply}
}

我的目标是允许我的库的用户序列化/反序列化案例类,而无需强迫他编写样板代码。目前,您必须使用上述 asProduct 方法声明案例类和隐式 val,才能在上下文中拥有一个 Datatype 实例。此隐式用于以下代码:

def toXmlrpc[T](datatype: T)(implicit serializer: Serializer[T]): NodeSeq =
  serializer.serialize(datatype)

def fromXmlrpc[T](value: NodeSeq)(implicit deserializer: Deserializer[T]): Deserialized[T] =
  deserializer.deserialize(value)

这是使用类型类进行序列化和反序列化的经典策略。

此刻,我已经掌握了如何通过Generic或LabelledGeneric 从案例类转换为HList。问题是,一旦我完成了这个转换,我可以如何调用方法fromXmlrpctoXmlrpc,就像 asProduct2 示例中一样。我没有关于案例类中属性类型的任何信息,因此,编译器找不到任何满足fromXmlrpctoXmlrpc的隐式。我需要一种方法来约束 HList 的所有元素在上下文中都具有隐式数据类型

由于我是 Shapeless 的初学者,我想知道获得此功能的最佳方式是什么。我有一些见解,但我绝对不知道如何使用 Shapeless 完成它。理想的情况是有一种方法可以从案例类的给定属性中获取类型,并将此类型显式传递给fromXmlrpctoXmlrpc。我想这不是可以做到的。

4

2 回答 2

15

首先,您需要为HList. 也就是说,您需要指定如何序列化H :: THNil

implicit def hconsDatatype[H, T <: HList](implicit hd: Datatype[H],
                                          td: Datatype[T]): Datatype[H :: T] =
  new Datatype[H :: T] {
    override def serialize(value: H :: T): NodeSeq = value match {
      case h :: t =>
        val sh = hd.serialize(h)
        val st = td.serialize(t)
        (sh ++ st).theSeq
    }

    override def deserialize(from: NodeSeq): Deserialized[H :: T] =
      (hd.deserialize(from.head) |@| td.deserialize(from.tail)) {
        (h, t) => h :: t
      }
  }

implicit val hnilDatatype: Datatype[HNil] =
  new Datatype[HNil] {
    override def serialize(value: HNil): NodeSeq = NodeSeq()
    override def deserialize(from: NodeSeq): Deserialized[HNil] =
      Success(HNil)
  }

然后,您可以为可以通过以下方式解构的任何类型定义通用序列化程序Generic

implicit def genericDatatype[T, R](implicit gen: Generic.Aux[T, R],
                                   rd: Lazy[Datatype[R]]): Datatype[T] =
  new Datatype[T] {
    override def serialize(value: T): NodeSeq =
      rd.value.serialize(gen.to(value))

    override def deserialize(from: NodeSeq): Deserialized[T] =
      rd.value.deserialize(from).map(rd.from)
  }

请注意,我必须使用Lazy,否则如果您有嵌套的案例类,此代码会破坏隐式解析过程。如果您遇到“发散隐式扩展”错误,您可以尝试在和中添加Lazy隐式参数。hconsDatatypehnilDatatype

这是有效的,因为Generic.Aux[T, R]链接了任意的 product-like typeTHListtype R。例如,对于这个案例类

case class A(x: Int, y: String)

shapeless 将生成一个Generic类型的实例

Generic.Aux[A, Int :: String :: HNil]

因此,您可以将序列化委托给递归定义Datatype的 s for HList,首先将数据转换为HListwith Generic。反序列化的工作方式类似但相反 - 首先将序列化形式读取到HList,然后将HList其转换为实际数据Generic

我可能在NodeSeq上面使用 API 时犯了几个错误,但我想它传达了大致的想法。

如果你想使用LabelledGeneric,代码会变得稍微复杂一些,如果你想处理用Coproducts 表示的密封特征层次结构,那就更复杂了。

我正在使用 shapeless 在我的库picopickle中提供通用序列化机制。我不知道有任何其他库可以使用无形来做到这一点。您可以尝试找到一些如何在这个库中使用 shapeless 的示例,但是那里的代码有些复杂。在无形示例中还有一个示例,即S-expressions

于 2015-04-20T21:30:09.317 回答
12

弗拉基米尔的回答很好,应该被接受,但也可以用Shapeless 的TypeClass机器做得更好一点。鉴于以下设置:

import scala.xml.NodeSeq
import scalaz._, Scalaz._

trait Serializer[T] {
  def serialize(value: T): NodeSeq
} 

trait Deserializer[T] {
  type Deserialized[T] = Validation[AnyErrors, T]
  type AnyError = Throwable
  type AnyErrors = NonEmptyList[AnyError]
  def deserialize(from: NodeSeq): Deserialized[T]
}

trait Datatype[T] extends Serializer[T] with Deserializer[T]

我们可以这样写:

import shapeless._

object Datatype extends ProductTypeClassCompanion[Datatype] {
  object typeClass extends ProductTypeClass[Datatype] {
    def emptyProduct: Datatype[HNil] = new Datatype[HNil] {
      def serialize(value: HNil): NodeSeq = Nil
      def deserialize(from: NodeSeq): Deserialized[HNil] = HNil.successNel
    }

    def product[H, T <: HList](
      dh: Datatype[H],
      dt: Datatype[T]
    ): Datatype[H :: T] = new Datatype[H :: T] {
      def serialize(value: H :: T): NodeSeq =
        dh.serialize(value.head) ++ dt.serialize(value.tail)

      def deserialize(from: NodeSeq): Deserialized[H :: T] =
       (dh.deserialize(from.head) |@| dt.deserialize(from.tail))(_ :: _)
    }

    def project[F, G](
      instance: => Datatype[G],
      to: F => G,
      from: G => F
    ): Datatype[F] = new Datatype[F] {
      def serialize(value: F): NodeSeq = instance.serialize(to(value))
      def deserialize(nodes: NodeSeq): Deserialized[F] =
        instance.deserialize(nodes).map(from)
    }
  }
}

一定要一起定义这些,这样它们就会得到适当的陪伴。

那么如果我们有一个案例类:

case class Foo(bar: String, baz: String)

以及案例类成员类型的实例(在本例中为String):

implicit object DatatypeString extends Datatype[String] {
  def serialize(value: String) = <s>{value}</s>
  def deserialize(from: NodeSeq) = from match {
    case <s>{value}</s> => value.text.successNel
    case _ => new RuntimeException("Bad string XML").failureNel
  }
}

我们自动得到一个派生实例Foo

scala> case class Foo(bar: String, baz: String)
defined class Foo

scala> val fooDatatype = implicitly[Datatype[Foo]]
fooDatatype: Datatype[Foo] = Datatype$typeClass$$anon$3@2e84026b

scala> val xml = fooDatatype.serialize(Foo("AAA", "zzz"))
xml: scala.xml.NodeSeq = NodeSeq(<s>AAA</s>, <s>zzz</s>)

scala> fooDatatype.deserialize(xml)
res1: fooDatatype.Deserialized[Foo] = Success(Foo(AAA,zzz))

这与 Vladimir 的解决方案大致相同,但它让 Shapeless 抽象了一些类型类实例派生的无聊样板,因此您不必弄脏Generic.

于 2015-04-20T22:32:13.757 回答