简体   繁体   English

Scala中ADT的通用派生,具有自定义表示

[英]Generic derivation for ADTs in Scala with a custom representation

I'm paraphrasing a question from the circe Gitter channel here. 我在这里解释了来自Circe Gitter频道的一个问题

Suppose I've got a Scala sealed trait hierarchy (or ADT) like this: 假设我有一个像这样的Scala密封特征层次结构(或ADT):

sealed trait Item
case class Cake(flavor: String, height: Int) extends Item
case class Hat(shape: String, material: String, color: String) extends Item

…and I want to be able to map back and forth between this ADT and a JSON representation like the following: ...我希望能够在此ADT和JSON表示之间来回映射,如下所示:

{ "tag": "Cake", "contents": ["cherry", 100] }
{ "tag": "Hat", "contents": ["cowboy", "felt", "black"] }

By default circe's generic derivation uses a different representation: 默认情况下,circe的泛型推导使用不同的表示:

scala> val item1: Item = Cake("cherry", 100)
item1: Item = Cake(cherry,100)

scala> val item2: Item = Hat("cowboy", "felt", "brown")
item2: Item = Hat(cowboy,felt,brown)

scala> import io.circe.generic.auto._, io.circe.syntax._
import io.circe.generic.auto._
import io.circe.syntax._

scala> item1.asJson.noSpaces
res0: String = {"Cake":{"flavor":"cherry","height":100}}

scala> item2.asJson.noSpaces
res1: String = {"Hat":{"shape":"cowboy","material":"felt","color":"brown"}}

We can get a little closer with circe-generic-extras: 我们可以通过circe-generic-extras更接近:

import io.circe.generic.extras.Configuration
import io.circe.generic.extras.auto._

implicit val configuration: Configuration =
   Configuration.default.withDiscriminator("tag")

And then: 然后:

scala> item1.asJson.noSpaces
res2: String = {"flavor":"cherry","height":100,"tag":"Cake"}

scala> item2.asJson.noSpaces
res3: String = {"shape":"cowboy","material":"felt","color":"brown","tag":"Hat"}

…but it's still not what we want. ......但它仍然不是我们想要的。

What's the best way to use circe to derive instances like this generically for ADTs in Scala? 什么是使用circe来为Scala中的ADT一般派生这样的实例的最佳方法是什么?

Representing case classes as JSON arrays 将案例类表示为JSON数组

The first thing to note is that the circe-shapes module provides instances for Shapeless's HList s that use an array representation like the one we want for our case classes. 首先要注意的是,circe-shapes模块为Shapeless的HList提供实例,这些实例使用类似于我们的case类所需的数组表示。 For example: 例如:

scala> import io.circe.shapes._
import io.circe.shapes._

scala> import shapeless._
import shapeless._

scala> ("foo" :: 1 :: List(true, false) :: HNil).asJson.noSpaces
res4: String = ["foo",1,[true,false]]

…and Shapeless itself provides a generic mapping between case classes and HList s. ...和Shapeless本身提供了案例类和HList之间的通用映射。 We can combine these two to get the generic instances we want for case classes: 我们可以将这两个结合起来,以获得我们想要的案例类的通用实例:

import io.circe.{ Decoder, Encoder }
import io.circe.shapes.HListInstances
import shapeless.{ Generic, HList }

trait FlatCaseClassCodecs extends HListInstances {
  implicit def encodeCaseClassFlat[A, Repr <: HList](implicit
    gen: Generic.Aux[A, Repr],
    encodeRepr: Encoder[Repr]
  ): Encoder[A] = encodeRepr.contramap(gen.to)

  implicit def decodeCaseClassFlat[A, Repr <: HList](implicit
    gen: Generic.Aux[A, Repr],
    decodeRepr: Decoder[Repr]
  ): Decoder[A] = decodeRepr.map(gen.from)
}

object FlatCaseClassCodecs extends FlatCaseClassCodecs

And then: 然后:

scala> import FlatCaseClassCodecs._
import FlatCaseClassCodecs._

scala> Cake("cherry", 100).asJson.noSpaces
res5: String = ["cherry",100]

scala> Hat("cowboy", "felt", "brown").asJson.noSpaces
res6: String = ["cowboy","felt","brown"]

Note that I'm using io.circe.shapes.HListInstances to bundle up just the instances we need from circe-shapes together with our custom case class instances, in order to minimize the number of things our users have to import (both as a matter of ergonomics and for the sake of keeping down compile times). 请注意,我正在使用io.circe.shapes.HListInstances将我们需要的实例与circe-shapes以及我们的自定义案例类实例捆绑在一起,以便最大限度地减少用户必须导入的内容(两者都是人体工程学问题和为了缩短编译时间。

Encoding the generic representation of our ADTs 编码ADT的通用表示

That's a good first step, but it doesn't get us the representation we want for Item itself. 这是一个很好的第一步,但它并没有让我们得到我们想要的Item本身。 To do that we need some more complex machinery: 为此,我们需要一些更复杂的机器:

import io.circe.{ JsonObject, ObjectEncoder }
import shapeless.{ :+:, CNil, Coproduct, Inl, Inr, Witness }
import shapeless.labelled.FieldType

trait ReprEncoder[C <: Coproduct] extends ObjectEncoder[C]

object ReprEncoder {
  def wrap[A <: Coproduct](encodeA: ObjectEncoder[A]): ReprEncoder[A] =
    new ReprEncoder[A] {
      def encodeObject(a: A): JsonObject = encodeA.encodeObject(a)
    }

  implicit val encodeCNil: ReprEncoder[CNil] = wrap(
    ObjectEncoder.instance[CNil](_ => sys.error("Cannot encode CNil"))
  )

  implicit def encodeCCons[K <: Symbol, L, R <: Coproduct](implicit
    witK: Witness.Aux[K],
    encodeL: Encoder[L],
    encodeR: ReprEncoder[R]
  ): ReprEncoder[FieldType[K, L] :+: R] = wrap[FieldType[K, L] :+: R](
    ObjectEncoder.instance {
      case Inl(l) => JsonObject("tag" := witK.value.name, "contents" := (l: L))
      case Inr(r) => encodeR.encodeObject(r)
    }
  )
}

This tells us how to encode instances of Coproduct , which Shapeless uses as a generic representation of sealed trait hierarchies in Scala. 这告诉我们如何编码的情况下, Coproduct ,这无形使用如Scala的密封特质层次的通用表示。 The code may be intimidating at first, but it's a very common pattern, and if you spend much time working with Shapeless you'll recognize that 90% of this code is essentially boilerplate that you see any time you build up instances inductively like this. 代码可能最初是令人生畏的,但这是一种非常常见的模式,如果您花费大量时间使用Shapeless,您会发现90%的代码基本上都是样板,您可以在任何时候以类似的方式构建实例。

Decoding these coproducts 解码这些副产品

The decoding implementation is a little worse, even, but follows the same pattern: 解码实现更糟糕,甚至,但遵循相同的模式:

import io.circe.{ DecodingFailure, HCursor }
import shapeless.labelled.field

trait ReprDecoder[C <: Coproduct] extends Decoder[C]

object ReprDecoder {
  def wrap[A <: Coproduct](decodeA: Decoder[A]): ReprDecoder[A] =
    new ReprDecoder[A] {
      def apply(c: HCursor): Decoder.Result[A] = decodeA(c)
    }

  implicit val decodeCNil: ReprDecoder[CNil] = wrap(
    Decoder.failed(DecodingFailure("CNil", Nil))
  )

  implicit def decodeCCons[K <: Symbol, L, R <: Coproduct](implicit
    witK: Witness.Aux[K],
    decodeL: Decoder[L],
    decodeR: ReprDecoder[R]
  ): ReprDecoder[FieldType[K, L] :+: R] = wrap(
    decodeL.prepare(_.downField("contents")).validate(
      _.downField("tag").focus
        .flatMap(_.as[String].right.toOption)
        .contains(witK.value.name),
      witK.value.name
    )
    .map(l => Inl[FieldType[K, L], R](field[K](l)))
    .or(decodeR.map[FieldType[K, L] :+: R](Inr(_)))
  )
}

In general there will be a little more logic involved in our Decoder implementations, since each decoding step can fail. 通常,我们的Decoder实现中将涉及更多逻辑,因为每个解码步骤都可能失败。

Our ADT representation 我们的ADT代表

Now we can wrap it all together: 现在我们可以将它们包装在一起:

import shapeless.{ LabelledGeneric, Lazy }

object Derivation extends FlatCaseClassCodecs {
  implicit def encodeAdt[A, Repr <: Coproduct](implicit
    gen: LabelledGeneric.Aux[A, Repr],
    encodeRepr: Lazy[ReprEncoder[Repr]]
  ): ObjectEncoder[A] = encodeRepr.value.contramapObject(gen.to)

  implicit def decodeAdt[A, Repr <: Coproduct](implicit
    gen: LabelledGeneric.Aux[A, Repr],
    decodeRepr: Lazy[ReprDecoder[Repr]]
  ): Decoder[A] = decodeRepr.value.map(gen.from)
}

This looks very similar to the definitions in our FlatCaseClassCodecs above, and the idea is the same: we're defining instances for our data type (either case classes or ADTs) by building on the instances for the generic representations of those data types. 这看起来与我们上面的FlatCaseClassCodecs的定义非常相似,并且这个想法是相同的:我们通过构建这些数据类型的泛型表示的实例来定义我们的数据类型(案例类或ADT)的实例。 Note that I'm extending FlatCaseClassCodecs , again to minimize imports for the user. 请注意,我正在扩展FlatCaseClassCodecs ,再次最小化用户的导入。

In action 在行动中

Now we can use these instances like this: 现在我们可以像这样使用这些实例:

scala> import Derivation._
import Derivation._

scala> item1.asJson.noSpaces
res7: String = {"tag":"Cake","contents":["cherry",100]}

scala> item2.asJson.noSpaces
res8: String = {"tag":"Hat","contents":["cowboy","felt","brown"]}

…which is exactly what we wanted. ......这正是我们想要的。 And the best part is that this will work for any sealed trait hierarchy in Scala, no matter how many case classes it has or how many members those case classes have (although compile times will start to hurt once you're into the dozens of either), assuming all of the member types have JSON representations. 最好的部分是,这将适用于Scala中任何密封的特征层次结构,无论它有多少个案例类或者这些案例类有多少成员(尽管编译时间一旦你进入数十个或者几个,就会开始受到伤害),假设所有成员类型都有JSON表示。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM