How statically typed meta programming can look
Join the DZone community and get the full member experience.
Join For FreeI want to start with piece of code and challenge the reader to understand what the code means. To make the task a bit fair, I am telling you that this is the full content of the file called .../grails-app/controllers/gppgrailstest/WebSocketChatController.groovy, which is obviously part of some Grails application.
def chatService index: { def id = request.session.id [ sessionId: id, userName: request.session.userName ?: (request.session.userName = chatService.newUserName()) ] }
OK, it is really hard to keep the intrigue for a long time and the code below is an exact copy of the script above after some AST transformations, which is Grails-aware.
If you are not aware about Groovy AST transformation you must learn it immediately. It is extremely powerful technique, which allows you to write compiler plugins in order to have very expressive domain specific languages. The general idea is that AST (abstract syntax tree) transformation modifies internal representation of your code on different stages of compilation.
package gppgrailstest @Typed class WebSocketChatController implements org.mbte.grails.languages.ControllerMethods { gppgrailstest.ChatService chatService static def defaultAction="index" Closure index = { def id = request.session.id [ sessionId: id, userName: request.session.userName ?: (request.session.userName = chatService.newUserName()) ] } }
On the first look, our transformation did not do anything non-trivial, but it did a lot. To be precise:
- we added package declaration by deducting package name from Grails convention
- we transformed the original script into controller class
- declaration of script top level local variable 'chatService' became property 'chatService'
- we found that our Grails application contains service bean called chatService of type gppgrailstest.ChatService and understood that property 'chatService' will be injected with this bean
- we transformed labeled block expression in to property index of type Closure
- we realized that as we have only one action in our controller it will be default one
- we added annotation @Typed to our controller class instructing that it must be compiled statically
- we added trait interface org.mbte.grails.languages.ControllerMethods to our controller
I can agree that AST transformation itself was not too complicated. In fact the full code for the transform is less than 110 lines of Groovy++. What is far from trivial is the fact that our code is really can be statically compiled.
There are at least two hard questions compiler need to resolve to be able to compile it:
- what do we mean by property 'request'
- even if we understood that 'request' is of type HttpServletRequest and 'request.session' is of type HttpSession and 'request.session.id' is of type String, it is still not clear what 'request.session.userName' means
The second question is a little bit easier, so we will start with it. In out previous article Groovy++ in action: statically typed dynamic dispatch we discussed powerful tecnique of handling unresolved methods and properties by the Groovy++ compiler. All we need to do is to add methods getUnresolvedProperty and setUnresolvedProperty to our class.
If HttpSession was class (in oppose to interface) and we could modify this class (which we can not any way) then all we need to do is to add two following methods.
def getUnresolvedProperty(String name) { getAttribute(name) } void setUnresolvedProperty(HttpSession session, String name, Object value) { setAttribute(name, value) }
Because HttpSession is interface we need to use extension (or category) methods
static def getUnresolvedProperty(HttpSession session, String name) { session.getAttribute(name) } static void setUnresolvedProperty(HttpSession session, String name, Object value) { session.setAttribute(name, value) }
Extension or category methods are static methods defining additional methods for some other type. The class to be extended is defined by the type of the first parameter and the parameters of added methods are defined by the rest of the parameters.
There are three ways in Groovy++ to make extension methods applicable during compilation
Groovy++ provides many extension methods for different servlet related classes.
- @Use annotation
- static methods defined in compiled class or it's superclasses
- globally by specially named registration file in the class path
So we are done with 'request.session.userName' and by using statically typed dynamic dispatch and extension methods it works now as a session attribute.
What about the 'request' property and all the other methods and properties which Grails usually provides to the controller via dynamic runtime meta programming? We need to make all these methods available in the controller class (and then in action closure). There are several ways to achive that
- We can try to introduce a common super class for all controllers and derive our controller from this super class. This is not so good if we want to reuse some controllers by inheritance.
- We can introduce an empty marker interface and define some extension methods for this interface as we did before. This is better, but we can not override the method in our controller.
- We can use traits.
The trait is Groovy++ (the concept is borrowed from Scala) and it is an interface with default implementations of some methods.
If a statically compiled class implements such an interface but does not provide its own implementation of such a method, the default implementation will be created automatically.
ControllerMethods is such a trait interface which provides all methods and properties that Grails adds to the controller. The beauty is that we use a static compiler and have both performance and compile time checks.
Voila. We have a fully statically typed controller. No black magic involved.
I hope it was interesting. You're welcome to learn more at the Groovy++ project page.
Thank you for reading and till next time.
Opinions expressed by DZone contributors are their own.
Trending
-
How to Handle Secrets in Kubernetes
-
Fun Is the Glue That Makes Everything Stick, Also the OCP
-
Hibernate Get vs. Load
-
MLOps: Definition, Importance, and Implementation
Comments