使用自定义表示在 Scala 中对 ADT 进行通用派生

2023-11-25

我正在转述来自 circe Gitter 频道的问题 here.

假设我有一个 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

…并且我希望能够在此 ADT 和 JSON 表示之间来回映射,如下所示:

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

默认情况下,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"}}

我们可以通过 circe-generic-extras 更接近一些:

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

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

进而:

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"}

……但这仍然不是我们想要的。

使用 circe 为 Scala 中的 ADT 派生此类实例的最佳方法是什么?


将案例类表示为 JSON 数组

首先要注意的是 circe-shapes 模块为 Shapeless 提供了实例HList使用数组表示形式,就像我们想要用于案例类的数组表示形式一样。例如:

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

…Shapeless 本身提供了案例类和案例类之间的通用映射HLists。我们可以将这两者结合起来以获得我们想要的案例类的通用实例:

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

进而:

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"]

请注意,我正在使用io.circe.shapes.HListInstances将 circe-shapes 所需的实例与自定义案例类实例捆绑在一起,以最大程度地减少用户必须导入的内容数量(无论是出于人体工程学问题还是为了缩短编译时间) 。

对 ADT 的通用表示进行编码

这是一个很好的第一步,但它并没有给我们带来我们想要的表现Item本身。为此,我们需要一些更复杂的机制:

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

这告诉我们如何对实例进行编码Coproduct,Shapeless 使用它作为 Scala 中密封特征层次结构的通用表示。这些代码一开始可能会令人生畏,但这是一种非常常见的模式,如果您花费大量时间使用 Shapeless,您会认识到此代码的 90% 本质上是样板文件,您在像这样归纳构建实例时会看到它们。

解码这些联产品

解码实现甚至有点差,但遵循相同的模式:

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(_)))
  )
}

一般来说,我们的逻辑会涉及更多一点Decoder实现,因为每个解码步骤都可能失败。

我们的 ADT 代表

现在我们可以将它们组合在一起:

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

这看起来与我们的定义非常相似FlatCaseClassCodecs上面的想法是相同的:我们通过构建这些数据类型的通用表示的实例来定义我们的数据类型(案例类或 ADT)的实例。请注意,我正在扩展FlatCaseClassCodecs,再次最大限度地减少用户的进口。

行动中

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

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"]}

……这正是我们想要的。最好的部分是,这适用于 Scala 中的任何密封特征层次结构,无论它有多少个案例类或这些案例类有多少个成员(尽管一旦您进入数十个其中之一,编译时间就会开始受到影响) ),假设所有成员类型都有 JSON 表示形式。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用自定义表示在 Scala 中对 ADT 进行通用派生 的相关文章

随机推荐

  • Django:ValueError:无法创建表单字段,因为尚未加载其相关模型

    我正在处理的 Django 项目遇到一些问题 我现在有两个应用程序 它们需要相当多的重叠 我真的只开始了第二个项目 称为workflow 我正在尝试为该申请制作第一份表格 我的第一个应用程序叫做po 在里面workflow应用程序我有一个名
  • “cout << cout” - 输出代表什么?

    经过一整天的编码后 我不小心写了 cout lt lt some text lt lt cout 代替 cout lt lt some text lt lt endl 现在它打印出一个内存地址 它指向什么 std cout是一个实例std
  • 如何使用 matplotlib.pyplot 更改图例字体大小

    这里有一个简单的问题 我正在尝试使用以下方法获取图例的大小matplotlib pyplot变小 即文本变小 我正在使用的代码是这样的 plot figure plot scatter k sum cf color black label
  • Spring OAuth:带有授权服务器后端的资源服务器

    我想开发两项独立的服务 一项用于业务 一项用于使用 Spring OAuth 2 进行用户身份验证 我们将它们称为业务服务和 OAuth 服务 现在 如果请求未经过身份验证 我希望将业务服务委托给 OAuth 服务 客户端应用程序 Andr
  • 如何将 ProcessBuilder 输出重定向到字符串?

    我正在使用以下代码来启动流程构建器 我想知道如何将其输出重定向到String ProcessBuilder pb new ProcessBuilder System getProperty user dir src generate lis
  • 检查 Ruby 中的整数数组是否递增

    我想检查排序后的数组值是否递增 1 例如 1 2 3 4 5 TRUE 1 2 8 9 10 FALSE 非常感谢任何建议 array 1 2 4 3 array sort each cons 2 all x y y x 1
  • 如何在字典理解中使用 if/else?

    Python 2 7 中是否存在一种方法可以制作如下内容 something if true if condition else something if false for key value in dict items 我知道你可以用
  • 设置 JAR 文件的类路径

    我最近刚刚使用 Eclipse 创建了 Java 项目 需要 2 个 JAR 文件 phiget21 jar 和 mysql jar 在 Eclipse 中运行程序时一切正常 并且我注意到 jar 文件保存在 lib 文件夹中 我很快就要将
  • DB2 MERGE 语句错误

    我尝试了以下几种变体 但仍然出现错误 有什么方法可以解决这个问题 DB2 10 1 用于 z OS V10 的 DB2 对于以下 MERGE INTO TRGT t USING SRC s ON t ACCTID s ACCTID AND
  • Swift 的 if let 是如何评估的?

    我在 Swift 网站和这里的各种帖子上看到了这段代码 我正在尝试掌握基础知识 这条线如何评价 if let name optionalName 我很困惑 因为它不是 name 可选名称 它正在分配值 那么它如何报告 true 和 当你用
  • 限制在给定时间开放的承诺数量

    以下 TypeScript 执行每个调用doSomething action 一次一个 这意味着列表中的第二项在第一项完成之前不会被调用 async performActionsOneAtATime for let action of li
  • 离线网络应用程序。如何存储数据?

    介绍 应用程序必须能够完全离线运行 在本地存储数据并在有互联网连接时通过 AJAX 将其发布到网上 这可能是几天后 问题 如何使用Javascript存储数据 补充笔记 我不想使用任何服务器端技术 它必须像数据库一样安全 我读过有关 coo
  • Rails:在 Rails 中使用带有 has_one 关联的构建

    在这个例子中 我创建了一个user没有profile 然后稍后创建一个profile对于该用户 我尝试使用构建has one协会但是爆炸了 我看到这个工作的唯一方法是使用has many The user应该最多只有一个profile 我一
  • 选项 和 for 理解

    我正在编写一个用于理解的代码 并且想知道一些事情 def updateUserStats user User Either Error User for stampleCount lt stampleRepository getStampl
  • Android重复提供者权限问题

    我们正在尝试发布一款休闲应用程序的付费无广告版本 该应用程序目前免费发布但带有广告 我们将所有包名称重构为com mycompanyname appname pro 市场上的免费版基本上没有 pro结尾 我们还进入了内容提供商并将权限更改为
  • 查找数据库上打开的连接数

    我的 Web 应用程序位于 asp net 2 0 c 2 0 和 sql server 208 中 我如何找到 sql server 2008 数据库上的打开连接数 有没有办法清除连接池 因为我的网站托管在共享主机上他们提供了有限的联系
  • Yii2:如何自定义 404 和 503 等错误页面

    我有以下 errorHandler 配置 errorHandler gt errorAction gt page error 在控制器页面中 在我想检查的操作错误中 我收到 404 错误 找不到页面 我怎样才能检查它 如果您正在尝试自定义E
  • 字符串文字存储在哪里,我可以修改它们吗?

    我对 C 中的字符串文字有几个问题 char strPtr Hello char strArray Hello Now strPtr and strArray被认为是字符串文字 根据我的理解 字符串文字存储在只读内存中 因此我们无法修改它们
  • Apiary.io - 具有不同参数的多个响应 (200)

    我试图通过不同的参数获得不同的响应 但有些东西不起作用 这是我的 API Question questions question id A Question object has the following attributes Param
  • 使用自定义表示在 Scala 中对 ADT 进行通用派生

    我正在转述来自 circe Gitter 频道的问题 here 假设我有一个 Scala 密封特征层次结构 或 ADT 如下所示 sealed trait Item case class Cake flavor String height