Как бесформенные классы 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
.