Groovy builders see Chapter 17, Groovy Builders, on page 260 are good examples of DSLs.. 18.5 Groovy and DSLs Groovy has a number of key capabilities to help create internal DSLs, includ
Trang 1TYPES OFDSLS 280
The previous code indicates that the alarm mock should returntrue on
the first call and throw an exception on the second
You can find another good example of a DSL in Grails/GORM For
example, you can specify data constraints on an object’s properties
using the following syntax:
class State
{
String twoLetterCode static constraints = {
twoLetterCode unique: true, blank: false, size: 2 2 }
}
Grails smartly recognizes this fluent and expressive syntax for
express-ing the constraints and generates the validation logic both for the front
end and for the back end
Groovy builders (see Chapter 17, Groovy Builders, on page 260) are
good examples of DSLs They’re fluent and built on context
18.3 Types of DSLs
When designing a DSL, you have to decide between two types—external
and internal
An external DSL defines a new language You have the flexibility to
choose the syntax You then parse the commands in your new language
to take actions When I took my first job, the company asked me to
maintain a DSL that needed extensive use oflexandyacc.4The parsing
was a lot of “fun.” You can use languages such as C++ and Java that
do heavyweight lifting for you with the support of extensive parsing
capabilities and libraries For example, you can use ANTLR to build
DSLs ([Par07])
An internal DSL, also called an embedded DSL, defines a new language
as well but within the syntactical confines of another languages You
don’t use any parsers, but you have to construe the syntax by tactfully
mapping to constructs such as methods and properties in the
underly-ing language The users of your internal DSL might not realize they’re
4 I first thought they asked me to do it because I was good I later understood they don’t
ask a new employee to do stuff because they’re good but because no one else wants to do
it!
Trang 2DESIGNINGINTERNALDSLS 281
using syntax of a broader language However, creating the internal DSL
takes significant design effort and clever tricks to make the underlying
language work for you
I mentioned Ant and Gant earlier Ant, which uses XML, is an example
of an external DSL Gant, on the other hand, uses Groovy to solve the
same problem and is an example of an internal DSL
18.4 Designing Internal DSLs
Dynamic languages are better suited to designing and implementing
internal DSLs They have good metaprogramming capabilities and
flex-ible syntax, and you can easily load and execute code fragments
Not all dynamic languages are created equal, however
I find it very easy to create DSLs in Ruby, for example It is dynamically
typed, parentheses are optional, the symbol (:) can be used instead of
double quoting strings, and so on The elegance of Ruby heavily favors
creating internal DSLs
Creating internal DSLs in Python can be a bit of a challenge The
sig-nificant whitespace can be a hindrance
Groovy’s dynamic typing and metaprogramming capabilities help a
great deal However, it’s picky about parentheses and does not have
the elegant symbol that Ruby does You will have to work around some
of these restrictions, as you’ll see later
It takes significant time, patience, and effort to design an internal DSL
So, be creative, tactfully work around issues, and be willing to
compro-mise at places to succeed in your design efforts
18.5 Groovy and DSLs
Groovy has a number of key capabilities to help create internal DSLs,
including the following:
• Dynamic and optional typing (Section 4.5, Optional Typing, on
page86)
• The flexibility to load scripts dynamically, manipulate, and
exe-cute (Section11.6, Using Groovy Scripts from Groovy, on page178)
Trang 3CLOSURES ANDDSLS 282
• Groovy classes are open, thanks to categories and
ExpandoMeta-Class (see Chapter 14, MOP Method Injection and Synthesis, on
page202)
• Closures provide a nice context for execution (Chapter 5, Using
Closures, on page92)
• Operator overloading helps freely define operators (Section 3.6,
Operator Overloading, on page56)
• Builder support (Chapter17, Groovy Builders, on page260)
• Flexible parentheses.5
In the rest of this chapter, you’ll look at examples of creating DSLs in
Groovy using these capabilities
18.6 Closures and DSLs
Theidentity( ) method helps delegate calls within a closure, giving you a
context of execution You can take advantage of this approach to create
your own methods with context and fluency
Let’s revisit the pizza-ordering example Say you want to create a syntax
that flows naturally You don’t want to create an instance of PizzaShop
because that is more of an implementation detail You want the context
to be implicit Let’s take a look at the following code (wait until the next
section to see how you can make this more fluent and context-driven):
printf "Pizza will arrive in %d minutes\n" , time
ThegetPizza( ) method accepts a closure within which you call methods
to order pizza using the instance methods of aPizzaShopclass However,
the instance of that class is implicit The delegate (see Section 5.8,
5 This is useful and annoying at the same time Groovy requires no parentheses for
calling methods that take parameters but insists on having them for methods with no
parameters See Section 18.8 , The Parentheses Limitation and a Workaround, on page 285
for a simple trick to work around this annoyance.
Trang 4METHODINTERCEPTION ANDDSLS 283
Closure Delegation, on page 107) takes care of routing the methods
to the implicit instance, as you can see in the implementation of the
followinggetPizza( ) method:
The output from executing the call to thegetPizza( ) code is as follows:
Pizza will arrive in 25 minutes
Wait a second, how did you get the timevalue printed in the output?
Because the last statement ingetPizza( ) was a call to the closure,
what-ever it returned,getPizza( ) returned The last statement within the
clo-sure is setCard( ), so its result was returned to the caller This DSL
imposes ordering: thesetCard( ) must be the last method called to order
pizza You can work on improving the interface so the ordering is more
obvious Also, you can replace calls to set methods likesetSize Size.LARGE
with assignment statements likesize = Size.LARGE, if you want
18.7 Method Interception and DSLs
You can implement the DSL for ordering pizza without really using a
PizzaShop class You can do that by purely intercepting method calls
Let’s first start with the code to order pizza (stored in a file named
It hardly looks like code It looks more like a data file However, that’s
pure Groovy code, and you’re going to execute it.6 But before that, you
have to perform a few tricks, er, I mean design your DSL
6 Everything you see in that file, except the strings in double quotes, are either method
names or variable names.
Trang 5METHODINTERCEPTION ANDDSLS 284
Let’s create a file namedGroovyPizzaDSL.groovyand in it define the
vari-ableslarge,thin, andvisa(other variables likesmall,thick,masterCardcan
be defined at will) Now define a methodacceptOrder( ) that will call into
a closure that will eventually execute your DSL Also implement the
methodMissing( ) method that will be called for any method that does not
exist (pretty much all methods called in your DSL fileorderPizza.dsl)
Download CreatingDSLs/GroovyPizzaDSL.groovy
def large = 'large'
def thin = 'thin'
def visa = 'Visa'
def Olives = 'Olives'
def Onions = 'Onions'
def Bell_Pepper = 'Bell Pepper'
println "Validation and processing performed here for order received:"
orderInfo.each { key, value ->
println "${key} -> ${value.join(', ')}"
}
}
You have to figure out a way to put these two files together and execute
You can do that quite easily (see Section 11.6, Using Groovy Scripts
from Groovy, on page178), as shown next InvokeGroovyShell, load the
previous two scripts, form into a cohesive script, and evaluate
Download CreatingDSLs/GroovyPizzaOrderProcess.groovy
def dslDef = new File( 'GroovyPizzaDSL.groovy' ).text
def dsl = new File( 'orderPizza.dsl' ).text
Trang 6THEPARENTHESESLIMITATION AND AWORKAROUND 285
The output from the previous code is as follows:
Validation and processing performed here for order received:
As you can see, designing and executing a DSL in Groovy (as in
order-pizza.dsl) is pretty easy if you know how to exploit its MOP capabilities
18.8 The Parentheses Limitation and a Workaround
Let’s leave the pizza example behind and move on to look at a simple
register This section will show how to create a DSL for a simple register,
the device that lets you total amounts Here is the first attempt to create
that:
Download CreatingDSLs/Total.groovy
value = 0
def clear() { value = 0 }
def add(number) { value += number }
def total() { println "Total is $value" }
In this code, you wrote total( ) and clear( ) instead of total and clear,
respectively Let’s drop the parentheses and try to calltotal:
Trang 7CATEGORIES ANDDSLS 286
Executing the previous code gives the following result:
org.codehaus.groovy.runtime.metaclass.MissingPropertyExceptionNoStack:
No such property: total for class: Total
Groovy thinks that the call to total refers to a (nonexistent) property
Working with a language to design a DSL is like playing with a
2-year-old: you don’t fight with the kid when he gets cranky; you go along
a little bit So, in this case, tell Groovy that it’s OK and work with it
Simply create the properties it wants:
value = 0
def getClear() { value = 0 }
def add(number) { value += number }
def getTotal() { println "Total is $value" }
You wrote properties with the namestotalandclearby writing the
meth-ods getTotal( ) andgetClear( ) Now, Groovy is quite happy (like the kid)
to play with us, and you can call these properties without parentheses:
Categories allow you to intercept method calls in a controlled fashion.7
You can put that to use in creating a DSL Let’s figure out ways to
implement the following fluent call:2.days.ago.at(4.30)
2is an instance ofInteger, and you know thatdaysis not a property on it
You’ll inject that, using categories, as a property (thegetDays( ) method)
Thedaysis just noise It provides connectivity in the sentence “two days
ago at 4.30.” You can implement the method getDays( ) that accepts
Integerand returns the received instance In the getAgo( ) method (for
the ago property), accept an instance of Integer, and return so many
7 See Section 14.1 , Injecting Methods Using Categories, on page 203
Trang 8CATEGORIES ANDDSLS 287
days before the current date using the operations on theCalendarclass
Finally, in the at( ) method, set the time on that date to the time given,
and return an instance of Date All this can be used within the use( )
block, as shown in the following code:8
Download CreatingDSLs/DSLUsingCategory.groovy
class DateUtil
{
static int getDays(Integer self) { self }
static Calendar getAgo(Integer self)
def hour = ( int )(time.doubleValue())
def minute = ( int )(Math.round((time.doubleValue() - hour) * 100))
A final concern with the DSL syntax created here is that you used
2.days.ago.at(4.30) It’s more natural to use 4:30 instead of 4.30, so it
would be nice to instead use 2.days.ago.at(4:30) Groovy allows you to
accept aMapas a parameter to methods
8 I’m not performing error checking on the time you provide, so you can send 4.70 if
you’d like instead of 5:10; it’s an undocumented feature Also, you may want to clone the
instance of Calendar given to you and modify the clone to avoid any side effects in other
places where you may use these methods.
Trang 9CATEGORIES ANDDSLS 288
By defining the parameter of the methodago( ) asMapinstead ofDouble,
you can achieve that, as shown here:
Download CreatingDSLs/DSLUsingCategory2.groovy
class DateUtil
{
static int getDays(Integer self) { self }
static Calendar getAgo(Integer self)
The only restriction in this approach using categories is that you can
use the DSL only within theuse( ) blocks This may not be such a severe
restriction It might actually be good because the method injection is
controlled Once you leave the block of code, the methods injected are
forgotten, which might be desirable In Section 18.10,
ExpandoMeta-Class and DSLs, on the next page, you will see how to implement the
same syntax usingExpandoMetaClass
Trang 10EXPANDOMETACLASS ANDDSLS 289
18.10 ExpandoMetaClass and DSLs
Categories take effect only within the use blocks, and their effect is
fairly limited in scope If you want the method injection to be effective
throughout your application, you can use theExpandoMetaClassinstead
of categories Let’s use ExpandoMetaClassto implement the DSL syntax
you saw in the previous section:
No signature of method: java.util.GregorianCalendar.at()
is applicable for argument types: (java.util.LinkedHashMap) values: {[4:30]}
The reason for this exception is that the method was added to the
interface Calendar, and by default ExpandoMetaClass does not provide
that to inheriting/implementing classes One solution is to add the
at( ) method to the class GregorianCalendar However, that would be
Trang 11EXPANDOMETACLASS ANDDSLS 290
wrong in principle since you’re not supposed to know the details of
the implementation while working at the level of interfaces You can
fix this problem by adding one line of code before any other code is
executed Put the following at the top of the code and rerun the code:
ExpandoMetaClass.enableGlobally() (see Section 14.2, Injecting Methods
Using ExpandoMetaClass, on page 208) The output from the previous
code after this change is as follows:
Fri Nov 23 04:30:00 MST 2007
As you learned in this chapter, creating an internal DSL in Groovy is
fairly easy The dynamic nature and optional typing allows you to create
a fluent interface Closures help you create context Groovy’s categories
and ExpandoMetaClass are helpful to inject, intercept, and synthesize
method calls and properties Finally, Groovy’s ability to load and
exe-cute arbitrary scripts comes in handy to exeexe-cute the DSLs
Trang 12Appendix A
Web Resources
Groovy Home .http://groovy.codehaus.org
Home of the Groovy project for documentation and downloads
Groovy Download Page http://groovy.codehaus.org/Download
Direct link to the Groovy download page for latest released version and previousversions
Groovy Daily Build http://build.canoo.com/groovy
Place to download current builds of Groovy project, if you like to stay on thebleeding edge
Groovy API Javadoc http://groovy.codehaus.org/api
Javadoc for the Groovy API
The GDK http://groovy.codehaus.org/groovy-jdk
List of the methods that are part of the Groovy JDK—Groovy extensions to theJDK
Markmail for Groovy Mailing List http://groovy.markmail.org
Convenient place to search for any topics discussed in the Groovy users mailinglist
Groovy Mailing Lists http://groovy.codehaus.org/Mailing+Lists
List and details of Groovy mailing lists
A Bit of Groovy History http://glaforge.free.fr/weblog/index.php?itemid=99
A blog by Guillaume Laforge on Groovy history
MetaClassand Method Interception .
.http://graemerocher.blogspot.com/2007/06/dynamic-groovy-groovys-equivalent-to html
A blog by Graeme Rocher on Groovy’s metaprogramming capabilities and openclasses