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:
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
.
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))