使用Shapeless HList轻松构建Json解码器。

问题描述 投票:0回答:1

我正在尝试编写我自己的轻量级玩具Json库,但我遇到了一个障碍,我试图想出一个简单的方法来指定一个 Encoder/Decoder. 我想我已经有了一个非常好的dsl语法,我只是不知道如何把它关闭。我想也许可以用 Shapeless HList但我从来没有使用过它,所以我画了一个空白,因为它将如何做.我的想法是链这些 has 召唤在一起,并建立了某种连锁的 HList[(String, J: Mapper)]如果有可能的话,可以在幕后尝试转换一个 JsonHList[J]这里是部分实现,以及我对使用它的想象。

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
    )
  }
json scala shapeless
1个回答
2
投票

试试吧

implicit class StringOp(s: String) {
  def :=[A](a: A): (String, A) = s -> a
}

implicit def strToJStr: String => Json.String = Json.String
implicit def dblToJNumber: Double => Json.Number = Json.Number
implicit def intToJNumber: Int => Json.Number = Json.Number(_)

sealed trait Json
object Json {
  case class Object(fields: (scala.Predef.String, Json)*) extends Json
  case class Array(items: List[Json]) extends Json
  case class String(value: scala.Predef.String) extends Json
  case class Number(value: Double) extends Json
  case class Boolean(value: scala.Boolean) extends Json
  case object Null extends Json
}

trait Mapper[J] {
  def encode(j: J): Json
  def decode(json: Json): Either[Json, J]
}

object Mapper {
  implicit val `object`: Mapper[Json.Object] = ???
  implicit val array: Mapper[Json.Array] = ???
  implicit val stringJson: Mapper[Json.String] = ???
  implicit val number: Mapper[Json.Number] = ???
  implicit val boolean: Mapper[Json.Boolean] = ???
  implicit val `null`: Mapper[Json.Null.type] = ???
  implicit val json: Mapper[Json] = ???
  implicit val int: Mapper[Int] = ???
  implicit val string: Mapper[String] = ???
  implicit val person: Mapper[Person] = ???

  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[A: Mapper](at: String): Builder[R, A :: HNil] =
      new Builder(strict, at :: Nil)
  }

  class Builder[R, L <: HList](strict: Boolean, l: List[String]) {
    def has[A: Mapper](at: String): Builder[R, A :: L] =
      new Builder(strict, at :: l)

    def is[L1 <: HList](decode: L1 => R)(encode: R => Json)(implicit
      reverse: ops.hlist.Reverse.Aux[L, L1]): Mapper[R] = {
      val l1 = l.reverse
      ???
    }    
  }
}

不幸的是,这需要 L1 明确指定为 is

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

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

否则 Error: missing parameter type for expanded function. The argument types of an anonymous function must be fully known.

改进推理的一个方法是将隐性的? reverse 归类 Builder 但这是效率较低的:一个 HList 每一步都会反过来,而不仅仅是最后一步。

另一种方法是引入帮助类

  def is(implicit reverse: ops.hlist.Reverse[L]) = new IsHelper[reverse.Out]

  class IsHelper[L1 <: HList]{
    def apply(decode: L1 => R)(encode: R => Json): Mapper[R] = {
      val l1 = l.reverse
      ???
    }
  }

但随后 apply 应明确

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

否则编译器就会误事 decode 作为 reverse.


1
投票

有一个很好的资源可以学习如何使用shapeless(HLIST加上LabelledGeneric)来达到这个目的。

Dave Gurnell的《Type Astronaut's Guide to Shapeless》。

在你的案例中,给定一个产品类型,如。

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

编译器应该访问该类型实例的名称和值。关于编译器如何能够在编译时创建JSON表示的解释在书中有很好的描述。

在你的例子中,你必须使用 标签式通用 并尝试创建一个通用的encoderdecoder。它是一个 种类 将您的类型创建为一个 HList,其中每个元素对应一个属性。

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

val genPerson = LabelledGeneric[Person]

编译器推导出以下类型。

/* 
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 encoderdecoder实例。下面的代码显示了创建一个通用的JSON编码器的步骤(本书第5章的总结),你可以自定义。

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

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编码器对象,使用本地的。

一个类型类来对你的类型进行编码。

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

对于第一个检查,你可以创建三个实例 这将是必要的Person类型。

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编码器实例。

import Instances._

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

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

gives:

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

很明显,你需要为每个case类创建实例。为了避免这种情况,你需要一个返回一个通用编码器的函数。

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

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

然后,你必须创建HList编码器。这需要一个隐式函数来创建HNil类型的编码器,以及另一个HList本身的编码器。

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实例注入一个编码器实例。它利用编译器的隐式解析来创建你的类型的LabeledGeneric并创建编码器实例。

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对象中对所有这些定义进行编码。 import Instances._

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

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

prints._

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

请注意,你需要在 HList 编码器中包含 Symbol 的 Witness 实例。这允许在运行时访问属性名。记住,你的Person类型的LabeledGeneric是这样的。

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

懒惰类型需要为递归类型创建编码器。

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

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

prints:

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进行编解码器推导的。

© www.soinside.com 2019 - 2024. All rights reserved.