当前位置: 首页 > 面试题库 >

如何在不区分对象的情况下带圆圈解码ADT

仲孙磊
2023-03-14
问题内容

假设我有一个这样的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}}

这种行为意味着,如果两个或多个案例类具有相同的成员名称,我们就不必担心歧义,但这并不总是我们想要的—有时我们知道展开的编码将是明确的,或者我们想通过指定顺序来消除歧义每个案例类都应该尝试,否则我们不在乎。

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

(这个问题经常出现,例如,今天早上在Gitter上与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 _)
}

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

另外值得一提的是我们的明确EncoderDecoder实例将优先于一般衍生的情况下,我们会以其他方式从一开始io.circe.generic.auto._进口(见我的幻灯片在这里为如何优先工作的一些讨论)。

我们可以像这样使用这些实例:

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}

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

更通用的解决方案

正如我在Gitter上所指出的,我们可以通过使用circe-
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}

这对于任何ADT任何地方工作,encodeAdtNoDiscr并且decodeAdtNoDiscr都在范围之内。如果我们希望它受到更大的限制,则可以A在这些定义中用ADT类型替换泛型,或者可以使定义成为非隐式的,并明确定义要以这种方式编码的ADT的隐式实例。

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

未来

通用扩展模块在这方面提供了更多的可配置性。例如,我们可以编写以下代码:

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采用a Option[String]代替a
String,以None表明我们不需要一个额外的字段来指示构造函数,从而为我们提供了与上一节中的圆形实例相同的行为。

如果您有兴趣看到这种情况,请提出问题,或者(甚至更好)提出请求。:)



 类似资料:
  • 假设我有这个方法: 我想调用这个方法,获取字符串,将该字符串转换为整数7,然后以非阻塞方式返回该整数。我该怎么做? 我试过这个,但是函数阻塞(同步): 我尝试使用代替(异步): 但是我得到了这个错误:<代码>类型不匹配:无法从int转换为Mono 那我该怎么办?

  • 我正在尝试使用ARCore在屏幕上不点击的情况下显示对象。在Google提供的ARCore Sceneform的基本示例中,您需要在屏幕检测到表面后点击屏幕。 我想实现这一点,AR在不点击屏幕的情况下显示对象。 我试着在不点击屏幕的情况下进行显示。 如果有人知道怎么做,请帮助我。提前感谢

  • 我正在实现一个排序列表类,在这个类中,我将对任何类型的对象进行排序,所以现在我想测试对象是否具有可比性, 我用这段代码覆盖了compareTo()方法:- 所以现在我需要给这些对象分配数字,像这样 但它总是给我一个例外:- 线程“main”中出现异常Java . lang . classcastexception:Java . lang . integer不能转换为java.lang.String

  • //类B扩展类并添加一个附加变量 //这是主类 在不使用铸造的情况下如何设计解决上述问题?很抱歉问了这个低级的问题(我是java新手)。

  • 问题内容: 我尝试的第一件事是创建一个静态库,但后来我发现尚不支持该库。Apple Xcode Beta 4发行说明: Xcode不支持构建包含Swift代码的静态库。(17181019) 我希望Apple能够在下一个Beta版本或GA版本中添加它,但我在他们的博客上阅读了以下内容: 在确保应用程序的运行时兼容性的同时,Swift语言本身将继续发展,并且二进制接口也将发生变化。为了安全起见,应使用

  • 问题内容: 当前,当我发出此SQL时,它将获得不同的用户名。 我有一些不同的用户名,它们代表组,例如。 我想将所有其他用户名(恰好是数字)分组为一个组,例如 我可以实现以下目标吗? 编辑:从答案修改的查询 问题答案: @bfavaretto很好(对他+1),但是如果您不了解前缀或前缀不同,则可以使用类似以下内容的方法: