-5

I'm trying to understand every piece of the implementation of class OWritesOps explained here: http://kailuowang.blogspot.mx/2013/11/addremove-fields-to-plays-default-case.html

The way scala / play doc is structured doesn't really help (in this area Java(doc) beats Scala(doc) hands down).

Let's start with: addField:

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

I could only guess that the ~ is an equivalent of "and" (I gather it from here: http://mandubian.com/2012/10/01/unveiling-play-2-dot-1-json-api-part2-writes-format-combinators/ )

Alright, so let's just take it this way: (writes ~ (__ \ fieldName).write[T]) create a new instance of Writes (combo).

What about (__ \ fieldName) ? After a lot of digging Play Doc, I came to a (shaky) conclusion that it produces an instance of JsPath? How did I arrive to that conclusion? It's for that .write[T] that follows; JsPath has that method.

Beside, I read somewhere that __ is a shortcut for JsPath (I forgot where, I guess it's in Play Doc as well, but using Scala doc it's almost impossible to find my way back to that page).

Ok..., so (writes ~ (__ \ fieldName).write[T]) produces an instance of OWrites. Next: what's this?: ((a: A) => (a, field(a)))

Oh..., it's for the apply the parameter to the "apply" method of OWrites; it takes an lambda function with the following signature ((A) => JsObject)

Ok...., so.... (a, field(a)) is supposed to create an instance of JsObject. So, I opened the play doc, JsObject. Hmm..., I guess it's this constructor (?): JsObject(fields: Seq[(String, JsValue)]) .... But,... I really doubt it.... I'm lost here.

Anyway, let's see an example how it is used:

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

So..., _.isAdult is supposed to map to a block with the following signature: A => T ... But..., if that's really the case, I was expecting {x => x.isAdult} .... so I guess _.isAdult is a syntactic-sugar provided by Scala (?).

So... back to the definition of addField: field(a) executes the block "_.isAdult". I can infer here that a is an instance of Person then.

Can you help me decipher all this?

Scala is cool, but it's doc system (still) sucks :(

The Archetypal Paul
  • 41,321
  • 20
  • 104
  • 134
Cokorda Raka
  • 4,375
  • 6
  • 36
  • 54

1 Answers1

3

Can you help me decipher all this?

I could only guess that the ~ is an equivalent of "and"

~ is the name of a method invoked on the instance of OWrites. writes ~ (__ \ fieldName).write[T]) is the same as writes.~((__ \ fieldName).write[T])). Note the brackets around the argument of ~.

There are two things relevant to this equivalent form:

  1. In Scala, any method with one argument can be written using infix notation. For example, if you have an instance list: java.util.ArrayList[Int], you could write list add 5.

  2. While a method name need not be alphanumeric (as in Java), not all operators have the same precedence. For example, 1 + 5 * 2 translates to 1.+(5.*(2)) and not to 1.+(5).*(2). Here's a summary of operator precedence in Scala.

However, OWrites has no method called ~ in its interface. If you import play.api.libs.functional.syntax._, there's an implicit conversion

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

and FunctionalBuilderOps defines the method

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

and an alias for it: def and[B](mb: M[B]): FunctionalBuilder[M]#CanBuild2[A, B] = this.~(mb)

What about (__ \ fieldName) ?

The package play.api.libs.json defines a value val __ = JsPath, so the double underscores are just an alias for the singleton object JsPath. The singleton object JsPath is not to be confused with the equally-named class. This construct is called a companion object in Scala. While Java puts static members next to the regular members in a class, Scala puts them inside the companion object.

Apparently, the object JsPath is at the same time the companion object of the case class JsPath and an instance of JsPath(List.empty).

fieldName is of type String and JsPath defines a method

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

which is equivalent to

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

The default apply method of a case class' companion object creates a new instance, so this is equivalent to

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

As our singleton object __ is an instance of JsPath with the empty list for path, this returns us a new JsPath(KeyPathNode(child)), where child == fieldName

what's this?: ((a: A) => (a, field(a)))

It's a function taking an instance of A. The function maps the argument a to the tuple (a, field(a), where field is a function provided as argument to your method addField. It has the signature field: A => T, so it is a function that takes an instance of A and returns an instance of T.

This may be a bit confusing, so let me give you an example function that does not operate on generics. Say you want to define a function f that squares an integer number. For example, f(1) should be 1, f(3) should be 9. As it takes an integer and returns an integer, its signature is f: Int => Int. It's implementation is f = (x: Int) => x * x.

Oh..., it's for the apply the parameter to the "apply" method of OWrites; it takes an lambda function with the following signature ((A) => JsObject)

Actually, this is not correct - the signatures do not match. The given lambda function is of type A => (A, T). Also, as pointed out in the first part of the answer, the method ~ returns an instance of FunctionalBuilder#CanBuild2. Unfortunatly, I am lost on what the called apply-method does, because it is highly generic and uses implicit arguments of a generic type.

so I guess _.isAdult is a syntactic-sugar provided by Scala (?).

Yes, the underscore in lambda functions is syntactic sugar for capturing the arguments. For example, _ + _ is syntactic sugar for (x, y) => x + y, meaning the following two values are equivalent:

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

So _.isAdult really just translates to {x => x.isAdult} and in the example, x: Person.

Update

The idea behind this write builder seems to be to provide means of defining a mapping from one of your Scala classes to JSON. Let's take a look again at the case class Person(name: String, age: Int). Now, let's assume we have an instance of Person:

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

and want to serialize the instance to the following JSON:

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

The key is always a string, but the value can be of a different type. So, basically, the serialization is a function taking a Person and returning a tuple of key-value pairs where the value is always a string, i.e., Person => ((String, String), (String, Int)). Or, in the general case where we do not know what we are going to serialize, simply that it has two fields, it is A => ((String, B), (String, C)) for arbitrary types A, B, and C.

This is what CanBuild2 emulates - the 2 standing for the number of members to serialize. There's also a CanBuild3, CanBuild4 and so on.

We can instantiate a CanBuild2 that is appropriate to our example, by calling

val serializer = (__ \ "name").write[String] ~ (__ \ "age").write[Int]

Here, the type of serializer is FunctionalBuilder[OWrites]#CanBuild2[String, Int]

Now, the apply method of CanBuild2 can be used to generate an instance of the first generic argument - OWrites. The argument to the apply method is a function that maps an instance of our type to serialize (here Person) to a tuple of the desired value types, here String and Int (representing name and age).

val personWriter: OWrites[Person] = serializer((p: Person) => (p.name, p.age))

This writer can now be used to generate a JSON string from any instance of Person. So we can pass our person and get the desired output:

println(toJson(odersky)(personWriter))
Community
  • 1
  • 1
Kulu Limpa
  • 3,501
  • 1
  • 21
  • 31
  • Thank you for taking the time understanding my confusion and clearing them up! About that ~ I also was guessing it's in the play.api.libs.functional.syntax._ ... But I couldn't find the doc for that "play.api.libs.functional.syntax" to confirm my guess (in the Play doc api, all I could find was "play.api.libs.functional").... So I guessed further, maybe "syntax" it's a package object, but I couldn't find any reference to it in the doc of "play.api.libs.functional". ... I really hope Scala doc gets some improvements. – Cokorda Raka Dec 11 '14 at 16:26
  • @user3443096 In this case I found the implicit conversion difficult to follow. Fortunatly, IntelliJ is good in resolving static method binding - even with implicits present - so `go to declaration` is a useful tool to find out which method is called statically. – Kulu Limpa Dec 11 '14 at 16:37
  • As the writer said in his blog, not much doc around.... I made another guess: so... a Writes made up of N Writes will take a tuple with N elements. I looked for "apply" method in Writes, couldn't find one with tuple as param. So, I opened my scala console: First I try this: [code] val myComboWrites = ((__ \ "a").write[String] ~ (__ \ "b").write[Int]) [/code] It gives: myComboWrites: play.api.libs.functional.FunctionalBuilder[play.api.libs.json.OWrites]#CanBuild2[String,Int] = play.api.libs.functional.FunctionalBuilder$CanBuild2@25a6a348 Next: [code] myComboWrites(("AAA", 2)) [/code] Error: – Cokorda Raka Dec 11 '14 at 17:00
  • :33: error: overloaded method value apply with alternatives: [B](f: B => (String, Int))(implicit fu: play.api.libs.functional.ContravariantFunctor[play.api.libs.json.OWrites])play.api.libs.json.OWrites[B] [B](f: (String, Int) => B)(implicit fu: play.api.libs.functional.Functor[play.api.libs.json.OWrites])play.api.libs.json.OWrites[B] cannot be applied to ((String, Int)) myComboWrites(("AAA", 2)) – Cokorda Raka Dec 11 '14 at 17:00
  • Oh... it's this "apply" in OWrites: apply[A](f: (A) ⇒ JsObject): OWrites[A] ... It takes a function that turns an object of type A to an object of type JsObject ... That's exactly why last night I made this guess that "(a, field(a))" is a way of instantiating a JsObject. But I wasn't very sure (no doc to confirm my guess). – Cokorda Raka Dec 11 '14 at 17:05
  • Haha... I'm equally lost in this "apply" of OWrites. – Cokorda Raka Dec 11 '14 at 17:08
  • This a small example, maybe can provide some hint: val myComboWritesBuilder = ((__ \ "a").write[String] ~ (__ \ "b").write[Int]); val myComboWrites = myComboWritesBuilder({a: String => (a, 2)}); toJson("dude") (myComboWrites); Result: {"a":"dude","b":2} – Cokorda Raka Dec 11 '14 at 17:18
  • After looking at this: https://www.playframework.com/documentation/2.3.x/api/scala/index.html#play.api.libs.functional.FunctionalBuilder and this: https://www.playframework.com/documentation/2.3.x/api/scala/index.html#play.api.libs.functional.FunctionalBuilder$CanBuild2 My conclusion for now: This (combo) Writes "generated" by FunctionalBuilder really does take a tuple. It applies each writes in the combo to each element in the tuple. I just hope Play-framework developers be more clear about this in their official doc (guide / tutorial). – Cokorda Raka Dec 11 '14 at 17:25
  • This gives another hint: scala> val myComboWrites = myComboWritesBuilder({a: String => (a, 2)}) myComboWrites: play.api.libs.json.OWrites[String] = play.api.libs.json.OWrites$$anon$2@2304b17e This special kind of "OWrites" (play.api.libs.json.OWrites$$anon$2) generates an anonymous class out of the tuple fed into it. This anonymous class generates "unapply" for the tuple. I've also tried: val myTupleWriters = Json.writes[(String, Int)] : error: No apply function found matching unapply parameters. So, this magic OWrites$$anon$2 provides that "unapply" func to the tuple passed in as param. – Cokorda Raka Dec 11 '14 at 17:41
  • @user3443096 I updated the answer to describe what I think is the usage of `CanBuild`. It's pretty hard to read your comments though, as code formatting does not work well in comments. – Kulu Limpa Dec 11 '14 at 18:59
  • You answer needs to be starred :) It's a bag of tricks doc-ed. – Cokorda Raka Dec 11 '14 at 19:20