使用 Shapeless HList 轻松构建 Json 解码器 [英] Using Shapeless HList to easily build Json Decoder

查看:47
本文介绍了使用 Shapeless HList 轻松构建 Json 解码器的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试编写我自己的小型轻量级玩具 Json 库,但我遇到了一个障碍,试图想出一种简单的方法来指定 Encoder/Decoder.我认为我有一个非常好的 dsl 语法,我只是不知道如何实现它.我认为使用 Shapeless HList 可能是可能的,但我以前从未使用过它,所以我画了一个空白来说明它是如何完成的.我的想法是将这些 has 调用链接在一起,并建立某种 HList[(String, J: Mapper)] 链,然后如果可能的话让它在幕后尝试将 Json 转换为 HList[J] 吗?这是实现的一部分,以及我想象的如何使用它:

I am working on trying to write my own little lightweight toy Json library, and I am running into a roadblock trying to come up with an easy way to specify an Encoder/Decoder. I think Ive got a really nice dsl syntax, Im just not sure how to pull it off. I think it might be possible using Shapeless HList, but Ive never used it before, so Im drawing a blank as to how it would be done. My thought was to chain these has calls together, and build up some sort of chain of HList[(String, J: Mapper)], and then if it is possible to have it behind the scenes try and convert a Json to a HList[J]? Here is part of the implementation, along with how I imagine using it:

trait Mapper[J] {

  def encode(j: J): Json

  def decode(json: Json): Either[Json, J]

}

object Mapper {

  def strict[R]: IsStrict[R] =
    new IsStrict[R](true)

  def lenient[R]: IsStrict[R] =
    new IsStrict[R](false)

  class IsStrict[R](strict: Boolean) {

    def has[J: Mapper](at: String): Builder[R, J] =
      ???

  }

  class Builder[R, T](strict: Boolean, t: T) {

    def has[J: Mapper](at: String): Builder[R, J] =
      ???

    def is(decode: T => R)(encode: R => Json): Mapper[R] =
      ???

  }
}

Mapper
  .strict[Person]
  .has[String]("firstName")
  .has[String]("lastName")
  .has[Int]("age")
  .is {
    case firstName :: lastName :: age :: HNil =>
      new Person(firstName, lastName, age)
  } { person =>
    Json.Object(
      "firstName" := person.firstName,
      "lastName" := person.lastName,
      "age" := person.age
    )
  }

推荐答案

有一个很好的资源可以学习如何为此目的使用 shapeless(HLIST 加 LabelledGeneric):

There is a wonderful resource to learn how to use shapeless(HLIST plus LabelledGeneric) for that purpose:

Dave Gurnell 的《The Type Astronaut’s Guide to Shapeless

就您而言,假设产品类型如下:

In your case, given a product type like:

case class Person(firstName: String, lastName: String, age: Int)

编译器应该访问该类型实例的名称和值.书中详细描述了编译器如何在编译时创建 JSON 表示.

The compiler should access to the names and the values of an instance of that type. The explanation about how the compiler is able to create a JSON representation at compile time is well described in the book.

在您的示例中,您必须使用 LabelledGeneric 并尝试创建通用编码器/解码器.它是一个类型类,它将类型的表示创建为 HList,其中每个元素对应一个属性.

In your example, you must use LabelledGeneric and try to create a generic encoder/decoder. It is a type class that creates a representation of your types as a HList where each element corresponds to a property.

例如,如果您为 Person 类型创建一个 LabeledGeneric

For example, if you create a LabeledGeneric for your Person type

val genPerson = LabelledGeneric[Person]

编译器推断出以下类型:

the compiler infers the following type:

/* 
shapeless.LabelledGeneric[test.shapeless.Person]{type Repr = shapeless.::[String with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("firstName")],String],shapeless.::[String with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("lastName")],String],shapeless.::[Int with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("age")],Int],shapeless.HNil]]]}
*/

因此,名称和值已经使用 Scala 类型表示,现在编译器可以在编译时派生 JSON 编码器/解码器实例.下面的代码显示了创建可以自定义的通用 JSON 编码器(本书第 5 章的摘要)的步骤.

So, the names and the values are already represented using Scala types and now the compiler can derive JSON encoder/decoder instances at compile time. The code below shows the steps to create a generic JSON encoder(a summary from the chapter 5 of the book) that you can customize.

第一步是创建一个 JSON 代数数据类型:

First step is to create a JSON algebraic data type:

sealed trait JsonValue
case class JsonObject(fields: List[(String, JsonValue)]) extends JsonValue
case class JsonArray(items: List[JsonValue]) extends JsonValue
case class JsonString(value: String) extends JsonValue
case class JsonNumber(value: Double) extends JsonValue
case class JsonBoolean(value: Boolean) extends JsonValue
case object JsonNull extends JsonValue

这一切背后的想法是,编译器可以获取您的产品类型并使用本机类型构建一个 JSON 编码器对象.

The idea behind all of this is that the compiler can take your product type and builds a JSON encoder object using the native ones.

用于编码类型的类型类:

A type class to encode your types:

trait JsonEncoder[A] {
   def encode(value: A): JsonValue
}

对于第一次检查,您可以创建三个 Person 类型所需的实例:

For a first check, you can create three instances that would be necessary for the Person type:

object Instances {

  implicit def StringEncoder : JsonEncoder[String] = new JsonEncoder[String] {
    override def encode(value: String): JsonValue = JsonString(value)
  }

  implicit def IntEncoder : JsonEncoder[Double] = new JsonEncoder[Double] {
    override def encode(value: Double): JsonValue = JsonNumber(value)
  }

  implicit def PersonEncoder(implicit strEncoder: JsonEncoder[String], numberEncoder: JsonEncoder[Double]) : JsonEncoder[Person] = new JsonEncoder[Person] {
    override def encode(value: Person): JsonValue =
      JsonObject("firstName" -> strEncoder.encode(value.firstName)
        :: ("lastName" -> strEncoder.encode(value.firstName))
        :: ("age" -> numberEncoder.encode(value.age) :: Nil))
  }
}

创建一个注入 JSON 编码器实例的编码函数:

Create an encode function that injects a JSON encoder instance:

import Instances._

def encode[A](in: A)(implicit jsonEncoder: JsonEncoder[A]) = jsonEncoder.encode(in)

val person = Person("name", "lastName", 25)
println(encode(person))

给出:

 JsonObject(List((firstName,JsonString(name)), (lastName,JsonString(name)), (age,JsonNumber(25.0))))

显然,您需要为每个案例类创建实例.为了避免这种情况,您需要一个返回通用编码器的函数:

Obviously you would need to create instances for each case class. To avoid that you need a function that returns a generic encoder:

def createObjectEncoder[A](fn: A => JsonObject): JsonObjectEncoder[A] =
  new JsonObjectEncoder[A] {
    def encode(value: A): JsonObject =
      fn(value)
  }

它需要一个函数 A -> JsObject 作为参数.这背后的直觉是编译器在遍历类型的 HList 表示时使用此函数来创建类型编码器,如 HList 编码器函数中所述.

It needs a function A -> JsObject as parameter. The intuition behind this is that the compiler uses this function when traversing the HList representation of your type to create the type encoder, as it is described in the HList encoder function.

然后,您必须创建 HList 编码器.这需要一个隐式函数来为 HNil 类型创建编码器,并为 HList 本身创建另一个.

Then, you must create the HList encoder. That requires an implicit function to create the encoder for the HNil type and another for the HList itself.

implicit val hnilEncoder: JsonObjectEncoder[HNil] =
    createObjectEncoder(hnil => JsonObject(Nil))

  /* hlist encoder */
implicit def hlistObjectEncoder[K <: Symbol, H, T <: HList](
    implicit witness: Witness.Aux[K],
    hEncoder: Lazy[JsonEncoder[H]],
    tEncoder: JsonObjectEncoder[T]): JsonObjectEncoder[FieldType[K, H] :: T] = {
    val fieldName: String = witness.value.name
    createObjectEncoder { hlist =>
      val head = hEncoder.value.encode(hlist.head)
      val tail = tEncoder.encode(hlist.tail)
      JsonObject((fieldName, head) :: tail.fields)
    }
  }

我们要做的最后一件事是创建一个隐式函数,为 Person 实例注入一个 Encoder 实例.它利用编译器隐式解析来创建您类型的 LabeledGeneric 并创建编码器实例.

The last thing that we have to do is to create an implicit function that injects an Encoder instance for a Person instance. It leverages the compiler implicit resolution to create a LabeledGeneric of your type and to create the encoder instance.

implicit def genericObjectEncoder[A, H](
     implicit generic: LabelledGeneric.Aux[A, H],
     hEncoder: Lazy[JsonObjectEncoder[H]]): JsonEncoder[A] =
     createObjectEncoder { value => hEncoder.value.encode(generic.to(value))
 }

您可以在 Instances 对象中对所有这些定义进行编码.导入实例._

You can code all these definitions inside the Instances object. import Instances._

val person2 = Person2("name", "lastName", 25)

println(JsonEncoder[Person2].encode(person2))

打印:

JsonObject(List((firstName,JsonString(name)), (lastName,JsonString(lastName)), (age,JsonNumber(25.0)))) 

请注意,您需要在 HList 编码器中包含 Symbol 的 Witness 实例.这允许在运行时访问属性名称.请记住,您的 Person 类型的 LabeledGeneric 类似于:

Note that you need to include in the HList encoder the Witness instance for Symbol. That allows to access the properties names at runtime. Remember that the LabeledGeneric of your Person type is something like:

String with KeyTag[Symbol with Tagged["firstName"], String] ::
Int with KeyTag[Symbol with Tagged["lastName"], Int] ::
Double with KeyTag[Symbol with Tagged["age"], Double] ::

需要为递归类型创建编码器的 Lazy 类型:

The Lazy type it is necessary to create encoders for recursive types:

case class Person2(firstName: String, lastName: String, age: Double, person: Person)

val person2 = Person2("name", "lastName", 25, person)

印刷品:

JsonObject(List((firstName,JsonString(name)), (lastName,JsonString(lastName)), (age,JsonNumber(25.0)), (person,JsonObject(List((firstName,JsonString(name)), (lastName,JsonString(name)), (age,JsonNumber(25.0)))))))

查看 Circe 或 Spray-Json 等库,了解它们如何使用 Shapeless 进行编解码器派生.

Take a look to libraries like Circe or Spray-Json to see how they use Shapeless for codec derivation.

这篇关于使用 Shapeless HList 轻松构建 Json 解码器的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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