Общий вывод для ADT в Scala с пользовательским представлением - PullRequest
0 голосов
/ 31 августа 2018

Я перефразирую вопрос из канала Цирце Гиттер здесь.

Предположим, у меня есть иерархия запечатанных черт 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 для общего получения таких экземпляров для ADT в Scala?

1 Ответ

0 голосов
/ 31 августа 2018

Представление классов дел в виде массивов JSON

Первое, на что нужно обратить внимание, это то, что модуль circe-shape предоставляет экземпляры для HList Shapeless, которые используют представление массива, подобное тому, которое мы хотим для наших классов case. Например:

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 обеспечивает общее отображение между классами case и HList s. Мы можем объединить эти два, чтобы получить универсальные экземпляры, которые мы хотим для case-классов:

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, чтобы связать только те экземпляры, которые нам нужны, из круговых фигур вместе с нашими экземплярами классов пользовательских падежей, чтобы минимизировать количество вещей, которые должны импортировать наши пользователи (оба по сути). эргономики и для экономии времени компиляции).

Кодирование общего представления наших 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.

...