如何使用属性和类型类的无形案例类?

时间:2015-04-20 20:10:54

标签: scala typeclass case-class shapeless hlist

我目前正在实现一个库,用于对XML-RPC消息进行序列化和反序列化。它几乎已经完成,但现在我正在尝试使用无形删除当前 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}
}

我的目标是允许我的库的用户序列化/反序列化case类,而不强迫他编写样板代码。目前,您必须使用前面提到的asProduct方法声明case类和隐式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。问题是,一旦我完成了这个转换,我就可以像在asProduct2示例中那样调用方法 fromXmlrpc toXmlrpc 。我没有关于case类中属性类型的任何信息,因此,编译器找不到任何满足 fromXmlrpc toXmlrpc 的隐式。我需要一种方法来约束HList的所有元素在上下文中都有一个隐式数据类型

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

2 个答案:

答案 0 :(得分: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]链接任意类似产品的类型THList类型R。例如,对于此案例类

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

无形将生成类型

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

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

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

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

我正在使用无形在我的库picopickle中提供通用序列化机制。我不知道有任何其他图书馆用无形的方式做到这一点。您可以尝试找到一些示例,如何在此库中使用无形状,但代码有些复杂。在无形的例子中也有一个例子,即S-expressions

答案 1 :(得分:12)

弗拉基米尔的答案很棒,应该是被接受的答案,但也可以用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)
    }
  }
}

请务必将它们全部定义在一起,这样它们才能得到适当的配合。

然后,如果我们有一个案例类:

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弄脏手。