Как бесформенные классы case с атрибутами и классами?

В настоящее время я реализую библиотеку для сериализации и десериализации сообщений XML-RPC и из них. Это почти сделано, но теперь я пытаюсь удалить шаблон моего текущего метода asProduct, используя Shapeless. Мой текущий код:

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, не заставляя его писать шаблонный код. В настоящее время вы должны объявить класс case и неявный val, используя вышеупомянутый метод asProduct, чтобы иметь экземпляр 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)

Это классическая стратегия сериализации и десериализации с использованием классов типов.

В этот момент я понял, как преобразовать из классов case в HList через Generic или LabelledGeneric. Проблема заключается в том, что когда я это преобразование сделал, как я могу вызвать методы fromXmlrpc и toXmlrpc, как в примере asProduct2. У меня нет никакой информации о типах атрибутов в классе case, и поэтому компилятор не может найти никаких неявных, которые удовлетворяют fromXmlrpc и toXmlrpc. Мне нужен способ ограничить, что все элементы HList имеют неявный тип данных в контексте.

Как я начинаю с Shapeless, я хотел бы знать, какой лучший способ получить эту функциональность. У меня есть некоторые идеи, но я определенно понятия не имею, как это сделать, используя Shapeless. Идеальным было бы получить способ получить тип от данного атрибута класса case и передать этот тип явно изXmlrpc и toXmlrpc. Я полагаю, что это не так, как это можно сделать.

Ответ 1

Во-первых, вам нужно написать родовые сериализаторы для HList. То есть вам нужно указать, как сериализовать H :: T и HNil:

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, потому что иначе этот код нарушит процесс неявного разрешения, если у вас есть вложенные классы case. Если вы получаете ошибки "расходящегося неявного расширения", вы можете попробовать добавить Lazy к неявным параметрам в hconsDatatype и hnilDatatype.

Это работает, потому что Generic.Aux[T, R] связывает произвольный тип типа T и HList type R. Например, для этого случая класс

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

formeless будет генерировать экземпляр Generic типа

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

Следовательно, вы можете делегировать сериализацию рекурсивно определенному Datatype для HList, сначала преобразуя данные в HList с Generic. Дессериализация работает аналогично, но наоборот: сначала сериализованная форма считывается на HList, а затем этот HList преобразуется в фактические данные с помощью Generic.

Возможно, я допустил несколько ошибок при использовании API NodeSeq выше, но я предполагаю, что он передает общую идею.

Если вы хотите использовать LabelledGeneric, код станет немного более сложным, и даже более того, если вы хотите обрабатывать запечатанные иерархии признаков, которые представлены Coproduct s.

Я использую бесформенное, чтобы обеспечить общий механизм сериализации в моей библиотеке, picopickle. Я не знаю ни одной другой библиотеки, которая делает это бесформенным. Вы можете попытаться найти несколько примеров того, как бесформенность может быть использована в этой библиотеке, но код там несколько сложный. Существует также пример среди бесформенных примеров, а именно S-выражения.

Ответ 2

Владимир ответ велик и должен быть признанным, но это также можно сделать немного лучше с 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:

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

И экземпляры для типов членов класса case (в данном случае просто 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] = [email protected]

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

Это работает примерно так же, как решение Владимира, но позволяет Shapeless абстрагироваться от скучного шаблона экземпляра экземпляра класса типа, поэтому вам не нужно загрязнять руки с помощью Generic.