【问题标题】:How to shapeless case classes with attributes and typeclasses?如何使用属性和类型类无形的案例类?
【发布时间】:2015-06-27 17:51:14
【问题描述】:

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

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

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

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

【问题讨论】:

    标签: scala typeclass case-class shapeless hlist


    【解决方案1】:

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

    请务必将所有这些定义在一起,这样它们才能得到适当的陪伴。

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

    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

    【讨论】:

      【解决方案2】:

      首先,您需要为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)
      

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

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

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

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

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-10-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-07-17
        • 2016-12-27
        • 2018-05-15
        相关资源
        最近更新 更多