如何使用属性和类型 类 来处理无形大小写 类?

How to shapeless case classes with attributes and typeclasses?

我目前正在实现一个库来对 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}
}

我的目标是让我的库的用户能够 serialize/deserialize 案例 classes 而不必强迫他编写样板代码。目前,您必须声明 case class 和使用上述 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)

这是使用类型classes序列化和反序列化的classic策略。

至此,我掌握了如何通过GenericLabelledGeneric将caseclasses转换为HList。问题是一旦完成此转换,我如何调用方法 fromXmlrpctoXmlrpc ,如 asProduct2 示例中所示。我没有关于 class 案例中属性类型的任何信息,因此,编译器找不到任何满足 fromXmlrpc 的隐式toXmlrpc。我需要一种方法来约束 HList 的所有元素在上下文中都具有隐式 Datatype

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

首先,您需要为 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,否则如果您有嵌套大小写 classes,此代码将破坏隐式解析过程。如果出现 "diverging implicit expansion" 错误,您可以尝试将 Lazy 添加到 hconsDatatypehnilDatatype 中的隐式参数。

这是有效的,因为 Generic.Aux[T, R] 链接了任意类似产品的类型 THList 类型 R。例如,对于这种情况 class

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

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

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

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

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

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

我正在使用 shapeless 在我的库中提供通用序列化机制,picopickle. I'm not aware of any other library which does this with shapeless. You can try and find some examples how shapeless could be used in this library, but the code there is somewhat complex. There is also an example among shapeless examples, namely S-expressions

Vladimir 的回答很好,应该被接受,但也可以用 Shapeless's TypeClass machinery 做得更好一点。给定以下设置:

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)
    }
  }
}

请务必将这些全部定义在一起,以便它们得到适当的陪伴。

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

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

和案例类型的实例 class 成员(在本例中只是 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@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 抽象了一些类型 class 实例派生的无聊样板,因此您不必弄脏 Generic .