Groovy++ in action: statically typed dynamic dispatch
Join the DZone community and get the full member experience.
Join For FreeIt already became common place to say that both dynamically and statically typed code have their own merits and drawbacks. Groovy builders (xml, markup or whatever) are huge example where dynamic dispatch wins big time compare to statically typed code in terms of expressiveness (except that it is not easy to mix typed and untyped code together) but lose in performance and static checks. Today I am going to show how we can build statically typed builder with Groovy++ without paying such penalties.
Groovy++ already contains unique feature to combine statically and dynamically typed code together. If you annotate your method (or class or whole script) with @Typed(TypePolicy.MIXED) then compiler will not complain about unresolved methods and properties but instead will generate dynamic invocations, which will be dispatch via meta class.
That still does not solve problems of type checking and performance. Even worse it might introduce new runtime bugs as we under impression that we are protected by compiler, so in this article we present different and better approach.
Let us start with example, which is inspired by Groovy Enhancement Proposal 7 - JSON Support.
We want to build and print in to standard output JSON representation of personal data of a person.
JsonClosure externalData = {
additionalData {
married true
conferences(['JavaOne', 'Gr8conf'])
projectRoles([
'Groovy' : 'Despot',
'Grails' : 'Commiter',
'Gaelyk' : 'Lead'
])
}
whaeverElseData (["xxxxxx", [x: 12, y:14], ['a', 'b', 'c']])
}
JsonBuilder2 builder = [new PrintWriter(System.out)]
builder.person {
firstName 'Guillaume'
lastName 'Laforge'
address (
city: 'Paris',
country: 'France',
zip: 12345,
)
externalData ()
}
And here is expected JSON output
{
"person" : {
"firstName" : "Guillaume",
"lastName" : "Laforge",
"address" : {
"city" : "Paris",
"country" : "France",
"zip" : 12345
},
"additionalData" : {
"married" : true,
"conferences" : [ "JavaOne", "Gr8conf" ],
"projectRoles" : {
"Groovy" : "Despot",
"Grails" : "Commiter",
"Gaelyk" : "Lead"
}
},
"whaeverElseData" : [ "xxxxxx", {
"x" : 12,
"y" : 14
}, [ "a", "b", "c" ] ]
}
}
Now we come to the most interesting question: how can such dynamic behavior be implemented in statically compiled language.
This is surprisingly simple: when compiler finds the method call, which it can not resolve, it starts looking for a method with special name "invokeUnresolvedMethod" which has absolutely any return type and absolutely any parameters as long as first parameter is of type String.
If the method not found in current class it check outer one (of course, if such outer class exists). That allows to use this technique in nested closures.
If such a method found compiler will try to generate code to invoke it. If not, it will provide compile time error.
The main beauty of this approach is that we can be as specific as we want about parameters and return types, so all power of type inference and type checking is at our disposal.
Let us see how it works. Our implementation of JsonBuilder is surprisingly simple
class JsonBuilder2 {
protected static ThreadLocal current = []
private final MappingJsonFactory factory = []
private final JsonGenerator gen
JsonBuilder2(Writer out) {
gen = factory.createJsonGenerator(out)
gen.useDefaultPrettyPrinter()
}
void call(JsonClosure obj) {
try {
current.set(gen)
gen.writeStartObject()
obj ()
gen.writeEndObject()
gen.close ()
}
finally {
current.remove()
}
}
void invokeUnresolvedMethod(String name, JsonClosure obj) {
call {
gen.writeObjectFieldStart name
obj ()
gen.writeEndObject()
}
}
}
Underneath we use brilliant Jackson framework for handling JSON.
The one, who is not familiar with Groovy/Groovy++, may note how easy to reuse existing Java libraries.
Our builder is very simplified, so we have only two forms of using it. The one is simplified as we used above and another one is more generic. We can rewrite example above in more generaic foram as
builder = [new PrintWriter(System.out)]
builder {
person {
firstName 'Guillaume'
lastName 'Laforge'
address (
city: 'Paris',
country: 'France',
zip: 12345,
)
externalData ()
}
}
It is very important to notice that there is huge difference between first and second form.
builder.person{...} means builder.invokeUnresolvedMethod("person", (JsonClosure){...}) and
builder {...} means builder.call((JsonClosure){...})
Now to complete our story we need to implement JsonClosure. It is a very staright forward and I will omit part of the code below. The whole code can be found at Groovy++ repository
abstract class JsonClosure {
protected JsonGenerator gen
abstract void define ()
void call () {
if(!gen)
gen = JsonBuilder2.current.get()
if(!gen)
throw new IllegalStateException("Can't use JsonClosure outside of JsonBuilder")
define ()
}
void invokeUnresolvedMethod(String name, Object obj) {
if(obj == null) {
gen.writeNullField name
return
}
switch(obj) {
case Closure:
gen.writeObjectFieldStart(name)
obj.call()
gen.writeEndObject()
break
case JsonClosure:
gen.writeObjectFieldStart(name)
obj.gen = gen
obj.define()
obj.gen = null
gen.writeEndObject()
break
case String:
gen.writeStringField(name, obj)
break
case Number:
gen.writeNumberField(name, obj)
break
case Map:
gen.writeObjectFieldStart(name)
for(e in obj.entrySet()) {
invokeUnresolvedMethod(e.key.toString(), e.value)
}
gen.writeEndObject()
break
case Iterable:
gen.writeArrayFieldStart(name)
iterate(obj)
gen.writeEndArray()
break
case Object []:
invokeUnresolvedMethod(name, obj.iterator())
break
case Boolean:
gen.writeBooleanField(name, obj)
break
default:
gen.writeObjectField(name, obj)
break
}
}
void iterate(Iterable obj) { ........... }
}
That's almost it. Of course, more involved builders can also use getUnresolvedProperty(String) and setUnresolvedProperty(String,SomeType) which allows to translate unresolved property access in to fast dynamic calls (This feature is not yet implemented in the trunk)
I hope it was interesting and maybe for someone open new view angle on dynamic dispatch.
Thank you for reading and till next time.
Opinions expressed by DZone contributors are their own.
Comments