The Language Applications Grid and Groovy DSLs
A Domain Specific Language (DSL) is a computer programming language specialized to a specific application domain. [1]
The Language Applications Grid uses several domain specific languages:
LSD is basically just Groovy bundled with the LAPPS API modules that are automatically imported into user scripts. The other three can be thought of executable configuration languages. The advantage of a DSL over a data format like Yaml or JSON is that it is executable code that can define functions, store commonly used values in variables, and make use of flow of control and looping statements.
It is all just Groovy
While the syntax may look odd at times, every DSL is a syntactically correct Groovy program. So even though the DSL may look like a configuration file it will actually be compiled in to Java bytecode and executed on the JVM. Consider a statement from the Discriminator DSL:
token {
uri 'http://vocab.lappsgrid.org/Token'
description 'A string of one or more characters that serves as an indivisible unit for the purposes of morpho-syntactic labeling (part of speech tagging).
}
The above calls a method named token
that takes a closure as its only parameter:
public void token(Closure body) { ... }
The closure contains two statements. The first statement calls the method uri
with a single String parameter and the second statement calls the method description
with a single String parameter.
Compiling Groovy at runtime
The GroovyShell
class makes it easy to compile a String into code that can be executed.
String code = 'println "hello world"'
def shell = new GroovyShell()
def script = shell.parse(code)
script.run()
// prints hello world
Groovy allows us to customize the compiler so we can do things like:
- Add import statements to the script
- Set a base class for the script
- Provide a Binding object to the script to inject variables.
Metaprogramming
Groovy provides several metaprogramming techniques that assist in writing a DSL. Two of the most useful are:
methodMissing(String,List)
allows us to synthesize methods dynamically. This is a powerful technique for creating Builders, that is, objects that build other objects based on some sort of description.- Closure delegates. A delegate is like a super class for a closure. If Groovy can not resolve a method call in the closure’s scope it will look in the delegate for the method.
Example DSL
Suppose we want to transform the following simplified Discriminator DSL into a JSON representation.
token {
uri 'http://vocab.lappsgrid.org/Token'
description 'A string of one or more characters that serves as an indivisible unit.'
}
sentence {
uri 'http://vocab.lappsgrid.org/Sentence'
description 'A sequence of one or more words.'
}
Our DSL processor will do three things:
- Implement a delegate class that provides the
uri
anddescription
methods. - Parse the code into a Script object.
- Implement
script.metaClass.methodMissing
to intercept method calls. - Generate the JSON from the constructed data structure.
1. The delegate class is simple enough.
The Delegate
class will provide the uri
and description
methods. Each method takes a String and will save it in a field of the same name.
class Delegate {
String uri
String description
void uri(String uri) { this.uri = uri }
void description(String description) { this.description = description }
}
2. Parsing the code
We have already seen how to do this above.
Script script = new GroovyShell().parse(code)
3. Handle missing methods
def objects= [:]
script.metaClass.methodMissing = { String name, args ->
Closure cl = args[0]
cl.delegate = new Delegate()
cl();
objects[name] = cl.delegate
}
First we create a HashMap
to hold the generated objects. We assume that the first parameter to the missing method will always be a closure, but in practice we would need to so some error checking here. After running the closure we add the Delegate
object to the map. In practice we would do some error checking here as well to make sure any required fields in the delegate were initialized and that fields were initialized with proper values.
4. Write the output
Groovy’s JsonBuilder
class makes the final step trivial.
println new groovy.json.JsonBuilder(objects).toPrettyString()
The finished product
Putting it all together the complete DSL processor looks like:
class Dsl {
public static void main(String[] params)
{
String code = new File(params[0]).text
def objects = [:]
Script script = new GroovyShell().parse(code)
script.metaClass.methodMissing = { String name, args ->
Closure cl = args[0]
cl.delegate = new Delegate()
cl();
annotations[name] = cl.delegate
}
script.run()
println new groovy.json.JsonBuilder(objects).toPrettyString()
}
}
class Delegate {
String uri
String description
void uri(String uri) { this.uri = uri }
void description(String description) { this.description = description }
}
If we did not implement script.metaClass.methodMissing
then Groovy would complain about the missing method token
when we tried to run the program on the example input. Similarly, if we did not add a delegate that implemented uri
and description
to the closure Groovy would complain about the missing method uri
when attempting to execute the closure.
References
- https://en.wikipedia.org/wiki/Domain-specific_language