如何在不区分对象的情况下带圆圈解码ADT [英] How to decode an ADT with circe without disambiguating objects

查看:89
本文介绍了如何在不区分对象的情况下带圆圈解码ADT的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

假设我有一个这样的ADT:

sealed trait Event

case class Foo(i: Int) extends Event
case class Bar(s: String) extends Event
case class Baz(c: Char) extends Event
case class Qux(values: List[String]) extends Event

circe 中的Decoder[Event]实例的默认通用派生期望输入JSON包含指示该情况的包装对象.类表示为:

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

scala> decode[Event]("""{ "i": 1000 }""")
res0: Either[io.circe.Error,Event] = Left(DecodingFailure(CNil, List()))

scala> decode[Event]("""{ "Foo": { "i": 1000 }}""")
res1: Either[io.circe.Error,Event] = Right(Foo(1000))

scala> (Foo(100): Event).asJson.noSpaces
res2: String = {"Foo":{"i":100}}

这种行为意味着,如果两个或多个case类具有相同的成员名称,我们就不必担心歧义,但这并不总是我们想要的-有时我们知道解开的编码将是模棱两可的,或者我们想通过以下方式消除歧义指定每个案例类应尝试的顺序,否则我们不在乎.

如何在没有包装的情况下(最好不必从头开始编写编码器和解码器)对Event ADT进行编码和解码?

(这个问题经常出现,例如,今天早上在吉特与与Igor Mazor的讨论).

解决方案

枚举ADT构造函数

获得所需表示的最直接方法是对案例类使用通用派生,但对ADT类型使用明确定义的实例:

import cats.syntax.functor._
import io.circe.{ Decoder, Encoder }, io.circe.generic.auto._
import io.circe.syntax._

sealed trait Event

case class Foo(i: Int) extends Event
case class Bar(s: String) extends Event
case class Baz(c: Char) extends Event
case class Qux(values: List[String]) extends Event

object Event {
  implicit val encodeEvent: Encoder[Event] = Encoder.instance {
    case foo @ Foo(_) => foo.asJson
    case bar @ Bar(_) => bar.asJson
    case baz @ Baz(_) => baz.asJson
    case qux @ Qux(_) => qux.asJson
  }

  implicit val decodeEvent: Decoder[Event] =
    List[Decoder[Event]](
      Decoder[Foo].widen,
      Decoder[Bar].widen,
      Decoder[Baz].widen,
      Decoder[Qux].widen
    ).reduceLeft(_ or _)
}

请注意,由于Decoder类型类不是协变的,因此必须在解码器上调用widen(由Cats的Functor语法提供,我们在第一次导入时就将其纳入了范围). circe类型类的不变性是一个有争议的问题(例如,Argonaut已从不变性变为协变并返回),但是它具有足够的优势,几乎不可能改变,这意味着我们偶尔需要这样的解决方法.

还值得注意的是,我们显式的EncoderDecoder实例将优先于我们从io.circe.generic.auto._导入获得的通用派生的实例(请参阅我的幻灯片

这可行,如果您需要指定尝试ADT构造函数的顺序,则它是当前最佳的解决方案.即使我们免费获得case类实例,必须枚举这样的构造函数显然也不理想.

更通用的解决方案

正如我在Gitter上注意到 一样,我们可以避免使用大写字母写出所有案例的大惊小怪-shapes模块:

import io.circe.{ Decoder, Encoder }, io.circe.generic.auto._
import io.circe.shapes
import shapeless.{ Coproduct, Generic }

implicit def encodeAdtNoDiscr[A, Repr <: Coproduct](implicit
  gen: Generic.Aux[A, Repr],
  encodeRepr: Encoder[Repr]
): Encoder[A] = encodeRepr.contramap(gen.to)

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

sealed trait Event

case class Foo(i: Int) extends Event
case class Bar(s: String) extends Event
case class Baz(c: Char) extends Event
case class Qux(values: List[String]) extends Event

然后:

scala> import io.circe.parser.decode, io.circe.syntax._
import io.circe.parser.decode
import io.circe.syntax._

scala> decode[Event]("""{ "i": 1000 }""")
res0: Either[io.circe.Error,Event] = Right(Foo(1000))

scala> (Foo(100): Event).asJson.noSpaces
res1: String = {"i":100}

这将适用于范围为encodeAdtNoDiscrdecodeAdtNoDiscr的任何ADT.如果我们希望它受到更大的限制,可以在这些定义中用ADT类型替换通用A,或者可以使定义非隐式,并为要以这种方式编码的ADT显式定义隐式实例.

此方法的主要缺点(除了额外的圆形依赖)是,构造函数将按字母顺序尝试,如果我们的模版类模棱两可(成员名称和类型不明确),这可能不是我们想要的一样).

未来

generic-extras模块在这方面提供了更多的可配置性.例如,我们可以编写以下代码:

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

implicit val genDevConfig: Configuration =
  Configuration.default.withDiscriminator("what_am_i")

sealed trait Event

case class Foo(i: Int) extends Event
case class Bar(s: String) extends Event
case class Baz(c: Char) extends Event
case class Qux(values: List[String]) extends Event

然后:

scala> import io.circe.parser.decode, io.circe.syntax._
import io.circe.parser.decode
import io.circe.syntax._

scala> (Foo(100): Event).asJson.noSpaces
res0: String = {"i":100,"what_am_i":"Foo"}

scala> decode[Event]("""{ "i": 1000, "what_am_i": "Foo" }""")
res1: Either[io.circe.Error,Event] = Right(Foo(1000))

我们有一个额外的字段来指示构造函数,而不是JSON中的包装对象.这不是默认行为,因为它有一些奇怪的极端情况(例如,如果我们的一个案例类具有名为what_am_i的成员),但是在许多情况下,这是合理的,并且自从引入该模块以来,它在通用扩展中得到了支持.

这仍然不能完全满足我们的需求,但是比默认行为要近.我也一直在考虑将withDiscriminator更改为Option[String]而不是String,其中None表示我们不需要额外的字段来表示构造函数,从而为我们提供了与circe相同的行为调整上一节中的实例.

如果您有兴趣看到这种情况,请打开问题,或者(甚至更好)打开拉动请求. :)

Suppose I've got an ADT like this:

sealed trait Event

case class Foo(i: Int) extends Event
case class Bar(s: String) extends Event
case class Baz(c: Char) extends Event
case class Qux(values: List[String]) extends Event

The default generic derivation for a Decoder[Event] instance in circe expects the input JSON to include a wrapper object that indicates which case class is represented:

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

scala> decode[Event]("""{ "i": 1000 }""")
res0: Either[io.circe.Error,Event] = Left(DecodingFailure(CNil, List()))

scala> decode[Event]("""{ "Foo": { "i": 1000 }}""")
res1: Either[io.circe.Error,Event] = Right(Foo(1000))

scala> (Foo(100): Event).asJson.noSpaces
res2: String = {"Foo":{"i":100}}

This behavior means that we never have to worry about ambiguities if two or more case classes have the same member names, but it's not always what we want—sometimes we know the unwrapped encoding would be unambiguous, or we want to disambiguate by specifying the order each case class should be tried, or we just don't care.

How can I encode and decodes my Event ADT without the wrapper (preferably without having to write my encoders and decoders from scratch)?

(This question comes up fairly often—see e.g. this discussion with Igor Mazor on Gitter this morning.)

解决方案

Enumerating the ADT constructors

The most straightforward way to get the representation you want is to use generic derivation for the case classes but explicitly defined instances for the ADT type:

import cats.syntax.functor._
import io.circe.{ Decoder, Encoder }, io.circe.generic.auto._
import io.circe.syntax._

sealed trait Event

case class Foo(i: Int) extends Event
case class Bar(s: String) extends Event
case class Baz(c: Char) extends Event
case class Qux(values: List[String]) extends Event

object Event {
  implicit val encodeEvent: Encoder[Event] = Encoder.instance {
    case foo @ Foo(_) => foo.asJson
    case bar @ Bar(_) => bar.asJson
    case baz @ Baz(_) => baz.asJson
    case qux @ Qux(_) => qux.asJson
  }

  implicit val decodeEvent: Decoder[Event] =
    List[Decoder[Event]](
      Decoder[Foo].widen,
      Decoder[Bar].widen,
      Decoder[Baz].widen,
      Decoder[Qux].widen
    ).reduceLeft(_ or _)
}

Note that we have to call widen (which is provided by Cats's Functor syntax, which we bring into scope with the first import) on the decoders because the Decoder type class is not covariant. The invariance of circe's type classes is a matter of some controversy (Argonaut for example has gone from invariant to covariant and back), but it has enough benefits that it's unlikely to change, which means we need workarounds like this occasionally.

It's also worth noting that our explicit Encoder and Decoder instances will take precedence over the generically-derived instances we'd otherwise get from the io.circe.generic.auto._ import (see my slides here for some discussion of how this prioritization works).

We can use these instances like this:

scala> import io.circe.parser.decode
import io.circe.parser.decode

scala> decode[Event]("""{ "i": 1000 }""")
res0: Either[io.circe.Error,Event] = Right(Foo(1000))

scala> (Foo(100): Event).asJson.noSpaces
res1: String = {"i":100}

This works, and if you need to be able to specify the order that the ADT constructors are tried, it's currently the best solution. Having to enumerate the constructors like this is obviously not ideal, though, even if we get the case class instances for free.

A more generic solution

As I note on Gitter, we can avoid the fuss of writing out all the cases by using the circe-shapes module:

import io.circe.{ Decoder, Encoder }, io.circe.generic.auto._
import io.circe.shapes
import shapeless.{ Coproduct, Generic }

implicit def encodeAdtNoDiscr[A, Repr <: Coproduct](implicit
  gen: Generic.Aux[A, Repr],
  encodeRepr: Encoder[Repr]
): Encoder[A] = encodeRepr.contramap(gen.to)

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

sealed trait Event

case class Foo(i: Int) extends Event
case class Bar(s: String) extends Event
case class Baz(c: Char) extends Event
case class Qux(values: List[String]) extends Event

And then:

scala> import io.circe.parser.decode, io.circe.syntax._
import io.circe.parser.decode
import io.circe.syntax._

scala> decode[Event]("""{ "i": 1000 }""")
res0: Either[io.circe.Error,Event] = Right(Foo(1000))

scala> (Foo(100): Event).asJson.noSpaces
res1: String = {"i":100}

This will work for any ADT anywhere that encodeAdtNoDiscr and decodeAdtNoDiscr are in scope. If we wanted it to be more limited, we could replace the generic A with our ADT types in those definitions, or we could make the definitions non-implicit and define implicit instances explicitly for the ADTs we want encoded this way.

The main drawback of this approach (apart from the extra circe-shapes dependency) is that the constructors will be tried in alphabetical order, which may not be what we want if we have ambiguous case classes (where the member names and types are the same).

The future

The generic-extras module provides a little more configurability in this respect. We can write the following, for example:

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

implicit val genDevConfig: Configuration =
  Configuration.default.withDiscriminator("what_am_i")

sealed trait Event

case class Foo(i: Int) extends Event
case class Bar(s: String) extends Event
case class Baz(c: Char) extends Event
case class Qux(values: List[String]) extends Event

And then:

scala> import io.circe.parser.decode, io.circe.syntax._
import io.circe.parser.decode
import io.circe.syntax._

scala> (Foo(100): Event).asJson.noSpaces
res0: String = {"i":100,"what_am_i":"Foo"}

scala> decode[Event]("""{ "i": 1000, "what_am_i": "Foo" }""")
res1: Either[io.circe.Error,Event] = Right(Foo(1000))

Instead of a wrapper object in the JSON we have an extra field that indicates the constructor. This isn't the default behavior since it has some weird corner cases (e.g. if one of our case classes had a member named what_am_i), but in many cases it's reasonable and it's been supported in generic-extras since that module was introduced.

This still doesn't get us exactly what we want, but it's closer than the default behavior. I've also been considering changing withDiscriminator to take an Option[String] instead of a String, with None indicating that we don't want an extra field indicating the constructor, giving us the same behavior as our circe-shapes instances in the previous section.

If you're interested in seeing this happen, please open an issue, or (even better) a pull request. :)

这篇关于如何在不区分对象的情况下带圆圈解码ADT的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆