解密Scala代码... JSON / Play框架/组合/功能/技巧/ OWrites

时间:2014-12-11 07:13:04

标签: json scala playframework

我试图理解这里解释的OWritesOps类的每一个实现:http://kailuowang.blogspot.mx/2013/11/addremove-fields-to-plays-default-case.html

scala / play doc的结构方式并没有什么帮助(在这方面Java(doc)胜过Scala(doc)。)

让我们从:addField:

开始
def addField[T: Writes](fieldName: String, field: A => T): OWrites[A] = 
(writes ~ (__ \ fieldName).write[T])((a: A) => (a, field(a)))

我只能猜测~相当于"和" (我从这里收集它:http://mandubian.com/2012/10/01/unveiling-play-2-dot-1-json-api-part2-writes-format-combinators/

好吧,让我们这样做吧:(writes ~ (__ \ fieldName).write[T])创建Writes(组合)的新实例。

(__ \ fieldName)怎么样?经过大量挖掘Play Doc之后,我得出了一个(摇摇欲坠的)结论,它产生了一个JsPath实例?我是如何得出这个结论的?它是.write[T]之后的结果; JsPath有这种方法。

旁边,我在某处读到__JsPath的快捷方式(我忘了在哪里,我猜它也在Play Doc中,但是使用Scala doc it' s几乎不可能找到回到那个页面的路。)

好的......,(writes ~ (__ \ fieldName).write[T])生成OWrites的实例。下一步:这是什么?:((a: A) => (a, field(a)))

哦......,将参数应用到" apply" OWrites的方法;它需要一个带有以下签名((A) => JsObject)

的lambda函数

好的....所以.... (a, field(a))应该创建JsObject的实例。所以,我打开了播放文档JsObject。嗯...,我猜它是这个构造函数(?):JsObject(fields: Seq[(String, JsValue)]) ....但是,......我真的怀疑它......我迷失在这里。

无论如何,让我们看一个如何使用它的例子:

val customWrites: Writes[Person] = Json.Writes[Person]. addField("isAdult", _.isAdult)

所以...,_.isAdult应该映射到具有以下签名的块:A => T ...但是......,如果真的如此,那我就是期待{x => x.isAdult} ....所以我猜_.isAdult是Scala(?)提供的语法糖。

所以...回到addField的定义:field(a)执行块" _。isAdult"。我可以在此推断aPerson的实例。

你能帮我解读这一切吗?

Scala很酷,但它的doc系统(仍然)很糟糕:(

1 个答案:

答案 0 :(得分:3)

你能帮我解读这一切吗?

我只能猜测〜相当于"和"

~是在OWrites实例上调用的方法的名称。 writes ~ (__ \ fieldName).write[T])writes.~((__ \ fieldName).write[T]))相同。 注意 ~参数周围的括号。

这个等价形式有两件事:

  1. 在Scala中,任何带有一个参数的方法都可以使用中缀表示法编写。例如,如果您有一个实例list: java.util.ArrayList[Int],则可以编写list add 5

  2. 虽然方法名称不必是字母数字(如Java中所示),但并非所有运算符都具有相同的优先级。例如,1 + 5 * 2转换为1.+(5.*(2))而不转换为1.+(5).*(2)Here's a summary of operator precedence in Scala

  3. 但是,OWrites在其界面中没有名为~的方法。如果您导入play.api.libs.functional.syntax._,则会进行隐式转化

    implicit def toFunctionalBuilderOps[M[_], A](a: M[A])(implicit fcb: FunctionalCanBuild[M]) = new FunctionalBuilderOps[M, A](a)(fcb)
    

    FunctionalBuilderOps定义方法

    def ~[B](mb: M[B]): FunctionalBuilder[M]#CanBuild2[A, B] = {
      val b = new FunctionalBuilder(fcb)
      new b.CanBuild2[A, B](ma, mb)
    }
    

    及其别名:def and[B](mb: M[B]): FunctionalBuilder[M]#CanBuild2[A, B] = this.~(mb)

    (__ \ fieldName)怎么样?

    play.api.libs.json定义了值val __ = JsPath,因此双下划线只是单例对象JsPath的别名。单例对象JsPath不应与同名的类混淆。此构造在Scala中称为伴随对象。虽然Java将静态成员放在类中的常规成员旁边,但Scala会将它们放在伴随对象中。

    显然,对象JsPath同时是案例类JsPath的伴随对象和JsPath(List.empty)的实例。

    fieldName的类型为StringJsPath定义了一种方法

    def \(child: String) = JsPath(path :+ KeyPathNode(child))
    

    相当于

    def \(child: String) = JsPath.apply(path :+ KeyPathNode(child))
    

    案例类的默认应用方法' companion对象创建一个新实例,因此这相当于

    def \(child: String) = new JsPath(path :+ KeyPathNode(child))
    

    由于我们的单件对象__JsPath的一个实例,其中包含path的空列表,因此我们返回new JsPath(KeyPathNode(child)),其中child == fieldName

    这是什么?:((a:A)=>(a,field(a)))

    它是一个以A为例的函数。该函数将参数a映射到元组(a, field(a),其中field是作为方法addField的参数提供的函数。它具有签名field: A => T,因此它是一个函数,它接受A的实例并返回T的实例。

    这可能有点令人困惑,所以让我给你一个不能在泛型上运行的示例函数。假设您要定义一个对整数进行平方的函数f。例如,f(1)应为1f(3)应为9。因为它需要一个整数并返回一个整数,所以它的签名是f: Int => Int。它的实施是f = (x: Int) => x * x

    哦......,将参数应用于"应用" OWrites的方法;它需要一个带有以下签名的lambda函数((A)=> JsObject)

    实际上,这是不正确的 - 签名不匹配。给定的lambda函数的类型为A => (A, T)。此外,正如答案的第一部分所指出的,方法~返回FunctionalBuilder#CanBuild2的实例。不幸的是,我迷失了所谓的apply - 方法的作用,因为它是高度通用的并且使用泛型类型的隐式参数。

    所以我猜_.isAdult是一个由Scala(?)提供的语法糖。

    是的,lambda函数中的下划线是用于捕获参数的语法糖。例如,_ + _(x, y) => x + y的语法糖,意味着以下两个值是等价的:

    val f: (Int, Int) => Int = (x, y) => x + y
    val g: (Int, Int) => Int = _ + _ 
    

    所以_.isAdult实际上只是转换为{x => x.isAdult},在示例中,x: Person

    更新

    这个写入构建器背后的想法似乎是提供定义从一个Scala类到JSON的映射的方法。让我们再看看case class Person(name: String, age: Int)。现在,我们假设我们有Person的实例:

    val odersky = Person("Martin Odersky", 56)
    

    并希望将实例序列化为以下JSON:

    {
      "name": "Martin Odersky", 
      "age": 56
    }
    

    密钥始终是字符串,但值可以是不同的类型。所以,基本上,序列化是一个函数,它取Person并返回一个键值对的元组,其中值总是一个字符串,即Person => ((String, String), (String, Int))。或者,在我们不知道序列化内容的一般情况下,只是它有两个字段,对于任意类型A => ((String, B), (String, C))A和{{B C 1}}。

    这是CanBuild2模仿的内容 - 2代表要序列化的成员数。还有CanBuild3CanBuild4等等。

    我们可以通过调用

    来实例化适合我们示例的CanBuild2
    val serializer = (__ \ "name").write[String] ~ (__ \ "age").write[Int]
    

    此处,serializer的类型为FunctionalBuilder[OWrites]#CanBuild2[String, Int]

    现在,CanBuild2的apply方法可用于生成第一个泛型参数的实例 - OWrites。 apply方法的参数是一个函数,它将我们类型的实例映射到序列化(此处为Person)到所需值类型的元组,此处为StringInt(表示{ {1}}和name)。

    age

    此编写器现在可用于从val personWriter: OWrites[Person] = serializer((p: Person) => (p.name, p.age)) 的任何实例生成JSON字符串。所以我们可以通过我们的人并获得所需的输出:

    Person