3. Guillaume Laforge
• Groovy Project Manager at VMware
• Initiator of the Grails framework
• Creator of the Gaelyk toolkit
• Co-author of Groovy in Action
• Speaking worldwide w/ a French accent
• Follow me on...
• My blog: http://glaforge.appspot.com
• Twitter: @glaforge
• Google+: http://gplus.to/glaforge
2 @glaforge — @paulk_asert
4. Guillaume Laforge
• Groovy Project Manager at VMware
• Initiator of the Grails framework
• Creator of the Gaelyk toolkit
• Co-author of Groovy in Action
• Speaking worldwide w/ a French accent
• Follow me on...
• My blog: http://glaforge.appspot.com
• Twitter: @glaforge
• Google+: http://gplus.to/glaforge
2 @glaforge — @paulk_asert
5. Paul King
• Core Groovy Committer
• Co-author of Groovy in Action
• Leads ASERT, a Brisbane, Australia, company providing
software development, training & mentoring support
• International speaker
• Follow me on
• Website: http://www.asert.com.au
• Twitter: @paulk_asert
3 @glaforge — @paulk_asert
6. Domain-Specific Languages
• Wikipedia definition
– A Domain-Specific Language is a programming language or
executable specification language that offers, through
appropriate notations and abstractions, expressive power
focused on, and usually restricted to, a particular problem
domain.
• In contrast to General Purprose Languages
• Somewhere between declarative data and GPLs
• Also known as: fluent / human interfaces, language oriented
programming, little or mini languages, macros, business
4 @glaforge — @paulk_asert
7. Goals of Domain-Specific Languages
• Use a more expressive language
than a general-purpose one
• Share a common metaphore of understanding
between develoeprs and subject matter experts
• Have domain experts help with the design
of the business logic of an application
• Avoid cluttering business code with too much boilerplate
technical code thanks to a clean seperation
5 @glaforge — @paulk_asert
9. Technical examples
Glade XSLT
<?xml version="1.0"?> <?xml version="1.0"?>
<GTK-Interface> <xsl:stylesheetversion="1.0"
<widget> xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<class>GtkWindow</class> <xsl:output method="xml"/>
<name>HelloWindow</name> <xsl:template match="*">
<border_width>5</border_width> <xsl:element name="{name()}">
<Signal> <xsl:for-each select="@*"> Regex
<xsl:element name="{name()}">
"x.z?z{1,3}y"
<name>destroy</name>
<handler>gtk_main_quit</handler> <xsl:value-of select="."/>
</Signal> </xsl:element>
<title>Hello</title> </xsl:for-each>
<type>GTK_WINDOW_TOPLEVEL</type> <xsl:apply-templates select="*|text()"/>
<position>GTK_WIN_POS_NONE</position> </xsl:element>
<allow_shrink>True</allow_shrink> </xsl:template>
<allow_grow>True</allow_grow> </xsl:stylesheet>
<auto_shrink>False</auto_shrink>
<widget>
<class>GtkButton</class> fetchmail
<name>Hello World</name>
<can_focus>True</can_focus> # Poll this site first each cycle.
<label>Hello World</label> poll pop.provider.net proto pop3
</widget> user "jsmith" with pass "secret1" is "smith" here
</widget> user jones with pass "secret2" is "jjones" here with options
</GTK-Interface> keep
SQL # Poll this site second, unless Lord Voldemort zaps us first.
poll billywig.hogwarts.com with proto imap:
SELECT * FROM TABLE user harry_potter with pass "floo" is harry_potter here
WHERE NAME LIKE '%SMI' # Poll this site third in the cycle.
ORDER BY NAME # Password will be fetched from ~/.netrc
poll mailhost.net with proto imap:
user esr is esr here
Troff
cat thesis.ms | chem | tbl | refer | grap | pic | eqn | groff -Tps > thesis.ps 8
Source: Applying minilanguages: http://www.faqs.org/docs/artu/ch08s02.html
@glaforge — @paulk_asert
10. Pros and cons of DSLs
• Pros – Safety; as long as the
– Domain experts can help, language constructs are safe,
validate, modify, and often any DSL sentence can be
develop DSL programs considered safe
– Somewhat self-documenting
– Enhance quality, • Cons
productivity, reliability, – Learning cost vs. limited
maintainability, portability, applicability
reusability – Cost of designing,
implementing & maintaining
DSLs as well as tools/IDEs
– Attaining proper scope
8 @glaforge — @paulk_asert
12. Scripts vs classes
• Hide all the boilerplate technical code
– an end-user doesn’t need to know about classes
10 @glaforge — @paulk_asert
13. Scripts vs classes
• Hide all the boilerplate technical code
– an end-user doesn’t need to know about classes
public class Rule {
public static void main(String[] args) {
System.out.println("Hello");
}
}
10 @glaforge — @paulk_asert
14. Scripts vs classes
• Hide all the boilerplate technical code
– an end-user doesn’t need to know about classes
public class Rule {
public static void main(String[] args) {
System.out.println("Hello");
}
}
println "Hello"
10 @glaforge — @paulk_asert
15. Optional typing
• No need to bother with types or even generics
– unless you want to!
– but strongly typed if you so desire
11 @glaforge — @paulk_asert
16. Optional typing
• No need to bother with types or even generics
– unless you want to!
– but strongly typed if you so desire
// given this API method
public Rate<LoanType, Duration, BigDecimal>[] lookupTable() { ... }
11 @glaforge — @paulk_asert
17. Optional typing
• No need to bother with types or even generics
– unless you want to!
– but strongly typed if you so desire
// given this API method
public Rate<LoanType, Duration, BigDecimal>[] lookupTable() { ... }
// verbose Java notation
Rate<LoanType, Duration, BigDecimal>[] table = lookupTable();
11 @glaforge — @paulk_asert
18. Optional typing
• No need to bother with types or even generics
– unless you want to!
– but strongly typed if you so desire
// given this API method
public Rate<LoanType, Duration, BigDecimal>[] lookupTable() { ... }
// verbose Java notation
Rate<LoanType, Duration, BigDecimal>[] table = lookupTable();
// leaner Groovy variant
def table = lookupTable()
11 @glaforge — @paulk_asert
24. Native syntax constructs
// Lists
def days = [Monday, Tuesday, Wednesday]
// Maps
def states = [CA: 'California', TX: 'Texas']
// Ranges (you can create your own)
12 @glaforge — @paulk_asert
25. Native syntax constructs
// Lists
def days = [Monday, Tuesday, Wednesday]
// Maps
def states = [CA: 'California', TX: 'Texas']
// Ranges (you can create your own)
def bizDays = Monday..Friday
12 @glaforge — @paulk_asert
26. Native syntax constructs
// Lists
def days = [Monday, Tuesday, Wednesday]
// Maps
def states = [CA: 'California', TX: 'Texas']
// Ranges (you can create your own)
def bizDays = Monday..Friday
def allowedAge = 18..65
12 @glaforge — @paulk_asert
27. Optional parens & semis
• Make statements and expressions
look more like natural languages
13 @glaforge — @paulk_asert
28. Optional parens & semis
• Make statements and expressions
look more like natural languages
move(left);
13 @glaforge — @paulk_asert
29. Optional parens & semis
• Make statements and expressions
look more like natural languages
move(left);
move left
13 @glaforge — @paulk_asert
30. Adding properties to numbers
• Several approaches to adding properties to numbers
– through a category
class PillsCategory {
static getPills(Number n) { n }
}
use(PillsCategory) {
2.pills // 2.getPills()
}
– through ExpandoMetaClass
Number.metaClass.getPills { ‐> delegate }
2.pills // 2.getPills()
14 @glaforge — @paulk_asert
31. Named arguments
• In Groovy you can mix named and unnamed arguments for
method parameters
– named params are actually put in a map parameter
– plus optional parens & semis
take 2.pills,
of: chloroquinine,
after: 6.hours
// Calls a method signature like:
def take(Map m, MedicineQuantity mq)
15 @glaforge — @paulk_asert
33. Command chain expressions
• A grammar improvement allowing you to
drop dots & parens when chaining method calls
– an extended version of top-level statements like println
16 @glaforge — @paulk_asert
34. Command chain expressions
• A grammar improvement allowing you to
drop dots & parens when chaining method calls
– an extended version of top-level statements like println
16 @glaforge — @paulk_asert
35. Command chain expressions
• A grammar improvement allowing you to
drop dots & parens when chaining method calls
– an extended version of top-level statements like println
• Less dots, less parens allow you to
– write more readable business rules
– in almost plain English sentences
• (or any language, of course)
16 @glaforge — @paulk_asert
36. Command chain expressions
• A grammar improvement allowing you to
drop dots & parens when chaining method calls
– an extended version of top-level statements like println
• Less dots, less parens allow you to
– write more readable business rules
– in almost plain English sentences
• (or any language, of course)
16 @glaforge — @paulk_asert
37. Command chain expressions
• A grammar improvement allowing you to
drop dots & parens when chaining method calls
– an extended version of top-level statements like println
• Less dots, less parens allow you to
– write more readable business rules
– in almost plain English sentences
• (or any language, of course)
• Let’s have a look at some examples
16 @glaforge — @paulk_asert
44. Command chain expressions
Before... we used to do...
take 2.pills, of: chloroquinine, after: 6.hours
19 @glaforge — @paulk_asert
45. Command chain expressions
Before... we used to do...
take 2.pills, of: chloroquinine, after: 6.hours
Normal argument
19 @glaforge — @paulk_asert
46. Command chain expressions
Before... we used to do...
take 2.pills, of: chloroquinine, after: 6.hours
Normal argument Named arguments
19 @glaforge — @paulk_asert
47. Command chain expressions
Before... we used to do...
take 2.pills, of: chloroquinine, after: 6.hours
Normal argument Named arguments
Woud call:
def take(Map m, Quantity q)
19 @glaforge — @paulk_asert
48. Command chain expressions
Now, even less punctuation!
take 2.pills of chloroquinine after 6.hours
20 @glaforge — @paulk_asert
49. Command chain expressions
Now, even less punctuation!
( ). ( ). ( )
take 2.pills of chloroquinine after 6.hours
20 @glaforge — @paulk_asert
50. Command chain expressions
// environment initialization
Integer.metaClass.getPills { ‐> delegate }
Integer.metaClass.getHours { ‐> delegate }
// variable injection
def chloroquinine = /*...*/
{ }
// implementing the DSL logic
def take(n) {
[of: { drug ‐>
[after: { time ‐> /*...*/ }]
}]
}
// ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐
take 2.pills of chloroquinine after 6.hours
21 @glaforge — @paulk_asert
52. Command chain expressions
take 2.pills of chloroquinine after 6.hours
... some dots remain ...
22 @glaforge — @paulk_asert
53. Command chain expressions
Yes, we can... get rid of them :-)
take 2 pills of chloroquinine after 6 hours
23 @glaforge — @paulk_asert
54. Command chain expressions
Yes, we can... get rid of them :-)
( ). ( ). ( ). ( )
take 2 pills of chloroquinine after 6 hours
23 @glaforge — @paulk_asert
55. Command chain expressions
// variable injection
def (of, after, hours) = /*...*/
// implementing the DSL logic
{ }
def take(n) {
[pills: { of ‐>
[chloroquinine: { after ‐>
['6': { time ‐> }]
}]
}]
}
// ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐
take 2 pills of chloroquinine after 6 hours
24 @glaforge — @paulk_asert
58. Command chain expressions
// methods with multiple arguments (commas)
take coffee with sugar, milk and liquor
@glaforge — @paulk_asert 25
59. Command chain expressions
// methods with multiple arguments (commas)
take coffee with sugar, milk and liquor
// leverage named‐args as punctuation
@glaforge — @paulk_asert 25
60. Command chain expressions
// methods with multiple arguments (commas)
take coffee with sugar, milk and liquor
// leverage named‐args as punctuation
check that: margarita tastes good
@glaforge — @paulk_asert 25
61. Command chain expressions
// methods with multiple arguments (commas)
take coffee with sugar, milk and liquor
// leverage named‐args as punctuation
check that: margarita tastes good
// closure parameters for new control structures
@glaforge — @paulk_asert 25
62. Command chain expressions
// methods with multiple arguments (commas)
take coffee with sugar, milk and liquor
// leverage named‐args as punctuation
check that: margarita tastes good
// closure parameters for new control structures
given {} when {} then {}
@glaforge — @paulk_asert 25
63. Command chain expressions
// methods with multiple arguments (commas)
take coffee with sugar, milk and liquor
// leverage named‐args as punctuation
check that: margarita tastes good
// closure parameters for new control structures
given {} when {} then {}
// zero‐arg methods require parens
@glaforge — @paulk_asert 25
64. Command chain expressions
// methods with multiple arguments (commas)
take coffee with sugar, milk and liquor
// leverage named‐args as punctuation
check that: margarita tastes good
// closure parameters for new control structures
given {} when {} then {}
// zero‐arg methods require parens
select all unique() from names
@glaforge — @paulk_asert 25
65. Command chain expressions
// methods with multiple arguments (commas)
take coffee with sugar, milk and liquor
// leverage named‐args as punctuation
check that: margarita tastes good
// closure parameters for new control structures
given {} when {} then {}
// zero‐arg methods require parens
select all unique() from names
// possible with an odd number of terms
@glaforge — @paulk_asert 25
66. Command chain expressions
// methods with multiple arguments (commas)
take coffee with sugar, milk and liquor
// leverage named‐args as punctuation
check that: margarita tastes good
// closure parameters for new control structures
given {} when {} then {}
// zero‐arg methods require parens
select all unique() from names
// possible with an odd number of terms
take 3 cookies
@glaforge — @paulk_asert 25
67. Command chain expressions
// methods with multiple arguments (commas)
take coffee with sugar, milk and liquor
( ). ( ). ( )
// leverage named‐args as punctuation
check that: margarita tastes good
// closure parameters for new control structures
given {} when {} then {}
// zero‐arg methods require parens
select all unique() from names
// possible with an odd number of terms
take 3 cookies
@glaforge — @paulk_asert 25
68. Command chain expressions
// methods with multiple arguments (commas)
take coffee with sugar, milk and liquor
( ). ( ). ( )
// leverage named‐args as punctuation
check that: margarita tastes good
( ). ( )
// closure parameters for new control structures
given {} when {} then {}
// zero‐arg methods require parens
select all unique() from names
// possible with an odd number of terms
take 3 cookies
@glaforge — @paulk_asert 25
69. Command chain expressions
// methods with multiple arguments (commas)
take coffee with sugar, milk and liquor
( ). ( ). ( )
// leverage named‐args as punctuation
check that: margarita tastes good
( ). ( )
// closure parameters for new control structures
given {} when {} then {}
( ). ( ). ( )
// zero‐arg methods require parens
select all unique() from names
// possible with an odd number of terms
take 3 cookies
@glaforge — @paulk_asert 25
70. Command chain expressions
// methods with multiple arguments (commas)
take coffee with sugar, milk and liquor
( ). ( ). ( )
// leverage named‐args as punctuation
check that: margarita tastes good
( ). ( )
// closure parameters for new control structures
given {} when {} then {}
( ). ( ). ( )
// zero‐arg methods require parens
select all unique() from names
( ). . ( )
// possible with an odd number of terms
take 3 cookies
@glaforge — @paulk_asert 25
71. Command chain expressions
// methods with multiple arguments (commas)
take coffee with sugar, milk and liquor
( ). ( ). ( )
// leverage named‐args as punctuation
check that: margarita tastes good
( ). ( )
// closure parameters for new control structures
given {} when {} then {}
( ). ( ). ( )
// zero‐arg methods require parens
select all unique() from names
( ). . ( )
// possible with an odd number of terms
take 3 cookies
( ).
@glaforge — @paulk_asert 25
72. Operator overloading
a + b // a.plus(b)
a ‐ b // a.minus(b)
• Currency amounts
a * b // a.multiply(b) – 15.euros + 10.dollars
a / b // a.divide(b)
a % b // a.modulo(b) • Distance handling
a ** b // a.power(b)
a | b // a.or(b) – 10.kilometers - 10.meters
a & b // a.and(b)
a ^ b // a.xor(b) • Workflow, concurrency
a[b] // a.getAt(b) – taskA | taskB & taskC
a << b // a.leftShift(b)
a >> b // a.rightShift(b)
+a // a.unaryPlus() • Credit an account
‐a // a.unaryMinus() – account << 10.dollars
~a // a.bitwiseNegate() account += 10.dollars
26 @glaforge — @paulk_asert
73. Builders
• For hierachical structures, extend:
– BuilderSupport
– FactoryBuilderSupport
• nice for extending builders with new nodes and attributes
import groovy.xml.*
• Roll your own.. def builder = new MarkupBuilder()
builder.html {
– with invokeMethod() head { title "Chicago!" }
or methodMissing() body {
div(id: "main") {
– with get/setProperty() p "Groovy rocks!"
}
or propertyMissing() }
}
27 @glaforge — @paulk_asert
74. AST Transformations
• Hook into the compiler process to do compile-time
metaprogramming by modifying the Abstract Syntax Tree
Transformation
28 @glaforge — @paulk_asert
75. @InheritConstructors
• Classes like Exception are painful when extended, as all
the base constructors should be replicated
class CustomException extends Exception {
CustomException() { super() }
CustomException(String msg) { super(msg) }
CustomException(String msg, Throwable t) { super(msg, t) }
CustomException(Throwable t) { super(t) }
}
29 @glaforge — http://glaforge.appspot.com — http://gplus.to/glaforge
76. @InheritConstructors
• Classes like Exception are painful when extended, as all
the base constructors should be replicated
import groovy.transform.*
@InheritConstructors
class CustomException extends Exception {
CustomException() { super() }
CustomException(String msg) { super(msg) }
CustomException(String msg, Throwable t) { super(msg, t) }
CustomException(Throwable t) { super(t) }
}
29 @glaforge — http://glaforge.appspot.com — http://gplus.to/glaforge
77. @InheritConstructor... under the covers
// ...
ClassNode sNode = cNode.getSuperClass();
for (ConstructorNode cn : sNode.getDeclaredConstructors()) {
Parameter[] params = cn.getParameters();
if (cn.isPrivate()) continue;
Parameter[] newParams = new Parameter[params.length];
List<Expression> args = copyParamsToArgs(params, newParams);
if (isClashing(cNode, newParams)) continue;
BlockStatement body = new BlockStatement();
ConstructorCallExpression callArgs =
new ConstructorCallExpression(ClassNode.SUPER,
new ArgumentListExpression(args));
body.addStatement(new ExpressionStatement(callArgs));
cNode.addConstructor(cn.getModifiers(), newParams,
cn.getExceptions(), body);
}
// ...
30 @glaforge — http://glaforge.appspot.com — http://gplus.to/glaforge
78. Advanced example with Spock
def "length of Spock's & his friends' names"() {
expect:
name.size() == length
where:
name | length
"Spock" | 5
"Kirk" | 4
"Scotty" | 6
}
31 @glaforge — http://glaforge.appspot.com — http://gplus.to/glaforge
81. Currency DSL…
From: customer@acme.org
To: Guillaume
Subject: Project Request
Dear Guillaume,
We would like a DSL for capturing currency
expressions. Just US currency to start with.
Thanks, Happy Customer
34 @glaforge — @paulk_asert
83. …Currency DSL…
From: customer@acme.org
To: Guillaume
Subject: Project Request
Dear Guillaume,
That works fairly well but can we get rid
of the ‘.value’ part of those expressions.
They are confusing our users.
Thanks, Happy Customer
36 @glaforge — @paulk_asert
86. …Currency DSL…
From: customer@acme.org
To: Guillaume
Subject: Project Request
Dear Guillaume,
Much better but our users are sometimes
using plural as well as singular
expressions. Is there anything that you
can do about that?
Thanks, Happy Customer
38 @glaforge — @paulk_asert
90. Business logic DSL
From: customer@acme.org
To: Paul King
Subject: Project Request
Dear Paul,
Could you please create us a small DSL for capturing
our business rules. We are thinking of something like:
price = 3
quantity = 10
total = price * quantity
Perhaps also an ability to check values:
assert total == 30
Thanks, Happy Customer
P.S. Will send a couple of more details in a follow‐up email but
please consider the requirements as being pretty much locked down.
41 @glaforge — @paulk_asert
92. Business logic DSL
From: customer@acme.org
To: Paul King
Subject: Project Request
Date: Mid morning
Dear Paul,
We were thinking a bit more about it, and it would be good to have
just a few more features. Hopefully, they won’t have much impact
on your existing design and can be added very quickly. It isn’t
much, just the ability to have IF, THEN, ELSE like structures, oh
yeah and the ability to have loops, and store stuff in files and
get stuff from databases and web services if we need.
Thanks, Happy Customer
P.S. It would be great if you can be finished by this afternoon.
We have a customer who would like this feature RSN. Thanks.
43 @glaforge — @paulk_asert
93. Business logic DSL
def shell = new GroovyShell()
shell.evaluate('''
price = 3
quantity = 10
total = price * quantity
assert total == 30
''')
44 @glaforge — @paulk_asert
94. Business logic DSL
From: customer@acme.org
To: Paul King
Subject: Project Request
Date: This afternoon
Dear Paul,
We were really happy with the DSL engine you provided us
with but people started importing all sorts of classes. Can
you stop them from doing that? Maybe just java.lang.Math
only. Also we decided we don’t like “while” loops anymore.
Leave you to it.
Thanks, Happy Customer
P.S. Have a beer and crab dinner for me at the conference.
Bye.
45 @glaforge — @paulk_asert
95. Business logic DSL
class CustomerShell {
Object evaluate(String text) {
try {
def loader = new CustomerClassLoader()
def clazz = loader.parseClass(text)
def script = clazz.newInstance()
return script.run()
} catch (...) { ... }
}
}
class CustomerClassLoader extends GroovyClassLoader {
def createCompilationUnit(CompilerConfiguration config, CodeSource codeSource) {
CompilationUnit cu = super.createCompilationUnit(config, codeSource)
cu.addPhaseOperation(new CustomerFilteringNodeOperation(), Phases.SEMANTIC_ANALYSIS)
return cu
}
}
private class CustomerFilteringNodeOperation extends PrimaryClassNodeOperation {
// ...
private static final allowedStaticImports = [Math].asImmutable()
void visitStaticMethodCallExpression(StaticMethodCallExpression smce) {
if (!allowedStaticImports.contains(smce.ownerType.getTypeClass())) {
throw new SecurityException("Static method call expressions forbidden in acme shell.")
}
}
void visitWhileLoop(WhileStatement whileStatement) {
throw new SecurityException("While statements forbidden in acme shell.")
}
// ...
}
46 @glaforge — @paulk_asert
Please also see the ArithmeticShell which is included under examples in the Groovy distribution
96. Business logic DSL
def shell = new CustomerShell()
shell.evaluate('''
price = 3
quantity = 10
total = price * quantity
assert total == 30
''')
47 @glaforge — @paulk_asert
98. Stock Exchange Order DSL
From: customer@finance‐broker.org
To: Guillaume
Subject: Project Request
Date: Wednesday morning
Dear Guillaume,
For our order processing system we have a need to capture
client orders using a DSL. An order includes the name of
the security to be transacted (buy or sell) as well as
quantity and unit price details to specify any constraint
that the counterparty would like to impose on the price
of transaction.
Thanks, Happy Customer
Example inspired
from «DSLs in Action»:
http://www.manning.com/ghosh/
49 @glaforge — @paulk_asert
99. // ‐‐‐‐‐ Implementation of the Fluent API ‐‐‐‐‐
… Stock Exchange Order DSL…
enum Action { Buy, Sell }
println new Order()
class Order { .sell(150, "IBM")
def security
def quantity, limitPrice .limitPrice(300)
boolean allOrNone
def valueCalculation
.allOrNone(true)
Action action .valueAs { qty, unitPrice ‐>
def buy(Integer quantity, String security) {
qty * unitPrice ‐ 100 }
this.quantity = quantity
this.security = security
this.action = Action.Buy
println new Order()
return this .buy(200, "GOOG")
}
def sell(Integer quantity, String security) { .limitPrice(200)
this.quantity = quantity .allOrNone(true)
this.security = security
this.action = Action.Sell .valueAs{ qty, unitPrice ‐>
return this qty * unitPrice ‐ 500 }
}
def limitPrice(Integer limit) {
this.limitPrice = limit; return this
} Sell 150 shares of IBM at valuation of 44900
def allOrNone(boolean allOrNone) {
this.allOrNone = allOrNone; return this Buy 200 shares of GOOG at valuation of 39500
}
def valueAs(Closure valueCalculation) {
this.valueCalculation = valueCalculation; return this
}
String toString() {
"$action $quantity shares of $security at valuation of ${valueCalculation(quantity, limitPrice)}"
}
}
50 @glaforge — @paulk_asert
100. Stock Exchange Order DSL
From: customer@finance‐broker.org
To: Guillaume
Subject: Project Request
Date: Wednesday morning
Dear Guillaume,
That version was great but can you make the
DSL a little more fluent. The brokers aren’t
programmers after all!
Thanks, Happy Customer
51 @glaforge — @paulk_asert
101. Stock Exchange Order DSL
class Order {
def security, quantity, limitPrice, allOrNone, value, bs
def buy(securityQuantity, closure) {
bs = 'Bought'
buySell(securityQuantity, closure)
}
def sell(securityQuantity, closure) {
bs = 'Sold'
buySell(securityQuantity, closure)
}
private buySell(securityQuantity, closure) {
// multiple assignment
(security, quantity) = [securityQuantity.security, securityQuantity.quantity]
// better clone the closure to avoid multi‐threading access issues
def c = closure.clone()
// delegate the method calls inside the closure to our methodMissing
c.delegationStrategy = Closure.DELEGATE_ONLY
c.delegate = this
def valuation = c()
println "$bs $quantity $security.name at valuation of $valuation"
}
// methods inside the closure will assign the Order properties
def methodMissing(String name, args) { this."$name" = args[0] }
52 @glaforge — @paulk_asert
102. Stock Exchange Order DSL
def getTo() { this } newOrder.to.buy(100.shares.of(IBM)) {
limitPrice 300
def valueAs(closure) { allOrNone true
value = closure(quantity, limitPrice) valueAs { qty, unitPrice ‐>
}
qty * unitPrice ‐ 200 }
}
}
class Security {
String name newOrder.to.sell 200.shares.of(GOOG), {
} limitPrice 200
class Quantity {
allOrNone false
Security security valueAs { qty, unitPrice ‐>
Integer quantity qty * unitPrice ‐ 500 }
} }
Integer.metaClass.getShares = { ‐> delegate }
Integer.metaClass.of = { new Quantity(security: it, quantity: delegate) }
class CustomBinding extends Binding {
def getVariable(String symbol) {
// create a new order each time
// for when you pass several orders
if (symbol == "newOrder") new Order()
// otherwise, it's an instrument
// trick to avoid using strings: use IBM instead of 'IBM'
else new Security(name: symbol)
}
}
// use the script binding for retrieving IBM, etc.
binding = new CustomBinding()
53 @glaforge — @paulk_asert
103. Stock Exchange Order DSL
From: customer@finance‐broker.org
To: Guillaume
Subject: Project Request
Date: Wednesday lunch time
Dear Guillaume,
That version was great but can we simplify the
constraint to remove the parameters, i.e.: change
{ qty, unitPrice ‐> qty * unitPrice ‐ 200 }
to this:
{ qty * unitPrice ‐ 200 }
Thanks, Happy Customer
54 @glaforge — @paulk_asert
104. Stock Exchange Order DSL
newOrder.to.buy(100.shares.of(IBM)) {
class Order {
limitPrice 300
...
allOrNone true
def allOrNone, valueCalculation, bs
valueAs { qty * unitPrice ‐ 200 }
...
private buySell(securityQuantity, closure) { }
...
valueCalculation = c() newOrder.to.sell 200.shares.of(GOOG), {
// debug print resulting order limitPrice 200
println toString() allOrNone false
return this valueAs { qty * unitPrice ‐ 500 }
} }
...
def valueAs(Closure wrapee) {
// in order to be able to define closures like { qty * unitPrice } without having
// to explicitly pass the parameters to the closure we can wrap the closure inside
// another one and that closure sets a delegate to the qty and unitPrice variables
def wrapped = { qty, unitPrice ‐>
def cloned = wrapee.clone()
cloned.resolveStrategy = Closure.DELEGATE_ONLY
cloned.delegate = [qty: qty, unitPrice: unitPrice]
cloned()
}
return wrapped
}
...
}
55 @glaforge — @paulk_asert
105. Stock Exchange Order DSL
From: customer@finance‐broker.org
To: Guillaume
Subject: Project Request
Date: Wednesday afternoon
Dear Guillaume,
Fantastic! This is getting better all the
time! But can we get rid most of the ‘.’ and
bracket symbols!
Thanks, Happy Customer
56 @glaforge — @paulk_asert
106. Stock Exchange Order DSL
// Characteristics of the order: "of GOOG {...}"
def of(SecurityAndCharacteristics secAndCharact) {
security = secAndCharact.security
def c = secAndCharact.characteristics.clone()
c.delegationStrategy = Closure.DELEGATE_ONLY
c.delegate = this
c()
// debug print of the resulting order
println toString()
return this
}
// Valuation closure: "of { qty, unitPrice ‐> ... }"
def of(Closure valueCalculation) {
// in order to be able to define closures like { qty * unitPrice }
// without having to explicitly pass the parameters to the closure
// we can wrap the closure inside another one
// and that closure sets a delegate to the qty and unitPrice variables
def wrapped = { qty, unitPrice ‐>
def cloned = valueCalculation.clone()
cloned.resolveStrategy = Closure.DELEGATE_ONLY
cloned.delegate = [qty: qty, unitPrice: unitPrice]
cloned()
}
return wrapped
}
}
57 @glaforge — @paulk_asert
http://groovyconsole.appspot.com/script/226001 by glaforge
107. Stock Exchange Order DSL
class Security {
String name
}
class SecurityAndCharacteristics {
Security security
Closure characteristics
}
class CustomBinding extends Binding {
def getVariable(String word) {
// return System.out when the script requests to write to 'out'
if (word == "out") System.out
// don't throw an exception and return null
// when a silent sentence word is used,
// like "to" and "the" in our DSL
null
}
}
// Script helper method for "GOOG {}", "VMW {}", etc.
def methodMissing(String name, args) {
new SecurityAndCharacteristics(
security: new Security(name: name),
characteristics: args[0]
)
}
// Script helper method to make "order to" silent
// by just creating our current order
def order(to) { new Order() }
// use the script binding for silent sentence words like "to", "the"
binding = new CustomBinding()
// syntax for 200.shares
Integer.metaClass.getShares = { ‐> delegate }
58 @glaforge — @paulk_asert
108. Stock Exchange Order DSL
order to buy 200.shares of GOOG {
limitPrice 500
allOrNone false
at the value of { qty * unitPrice ‐ 100 }
}
order to sell 150.shares of VMW {
limitPrice 80
allOrNone true
at the value of { qty * unitPrice }
}
59 http://groovyconsole.appspot.com/script/226001
@glaforge — @paulk_asert by glaforge
109. Stock Exchange Order DSL
From: customer@finance‐broker.org
To: Guillaume
Subject: Project Request
Date: Wednesday evening
Dear Guillaume,
Brilliant! Even our CEO could write orders!
Thanks, Happy Customer
60 @glaforge — @paulk_asert
111. Einstein’s Riddle DSL …
From: customer@acme.org
To: Paul King
Subject: Project Request
Date: Early morning
Dear Paul,
We would like a DSL for capturing our business
rules. Our business rules are captured in the form
of logic clauses. They are very much like those
found in Einstein’s riddle.
Thanks, Happy Customer
62 @glaforge — @paulk_asert
112. Einstein’s Riddle
• Wikipedia: The zebra puzzle is a well-known logic puzzle
– It is often called Einstein's Puzzle or Einstein's Riddle because it
is said to have been invented by Albert Einstein as a boy, with
the claim that Einstein said “only 2 percent of the world's
population can solve it.”
– The puzzle is also sometimes attributed to Lewis Carroll.
However, there is no known evidence for Einstein's or Carroll's
authorship; and the original puzzle cited below mentions brands
of cigarette, such as Kools, that did not exist during Carroll's
lifetime or Einstein's boyhood
63 @glaforge — @paulk_asert
113. Einstein’s Riddle
• Some premises:
– The British person lives in the red house
– The Swede keeps dogs as pets
– The Dane drinks tea
– The green house is on the left of the white house
– The green homeowner drinks coffee
– The man who smokes Pall Mall keeps birds
– The owner of the yellow house smokes Dunhill
– The man living in the center house drinks milk
– The Norwegian lives in the first house
– The man who smokes Blend lives next to the one who keeps cats
– The man who keeps the horse lives next to the man who smokes Dunhill
– The man who smokes Bluemaster drinks beer
– The German smokes Prince
– The Norwegian lives next to the blue house
64 @glaforge — @paulk_asert
114. Einstein’s Riddle
• Some premises:
– The British person lives in the red house • And a question:
– The Swede keeps dogs as pets – Who owns the fish?
– The Dane drinks tea
– The green house is on the left of the white house
– The green homeowner drinks coffee
– The man who smokes Pall Mall keeps birds
– The owner of the yellow house smokes Dunhill
– The man living in the center house drinks milk
– The Norwegian lives in the first house
– The man who smokes Blend lives next to the one who keeps cats
– The man who keeps the horse lives next to the man who smokes Dunhill
– The man who smokes Bluemaster drinks beer
– The German smokes Prince
– The Norwegian lives next to the blue house
64 @glaforge — @paulk_asert
115. Einstein’s Riddle : Prolog
% from http://www.baptiste‐wicht.com/2010/09/solve‐einsteins‐riddle‐using‐prolog
% Preliminary definitions
persons(0, []) :‐ !.
persons(N, [(_Men,_Color,_Drink,_Smoke,_Animal)|T]) :‐ N1 is N‐1, persons(N1,T).
person(1, [H|_], H) :‐ !.
person(N, [_|T], R) :‐ N1 is N‐1, person(N1, T, R).
% The Brit lives in a red house
hint1([(brit,red,_, _, _)|_]).
hint1([_|T]) :‐ hint1(T).
% The Swede keeps dogs as pets
hint2([(swede,_,_,_,dog)|_]).
hint2([_|T]) :‐ hint2(T).
% The Dane drinks tea
hint3([(dane,_,tea,_,_)|_]).
hint3([_|T]) :‐ hint3(T).
% The Green house is on the left of the White house hint4([(_,green,_,_,_),(_,white,_,_,_)|_]).
hint4([_|T]) :‐ hint4(T).
% The owner of the Green house drinks coffee.
hint5([(_,green,coffee,_,_)|_]).
hint5([_|T]) :‐ hint5(T).
...
65 @glaforge — @paulk_asert
116. Einstein’s Riddle DSL
From: customer@acme.org
To: Paul King
Subject: Project Request
Date: Early morning
Dear Paul,
Thanks for your Prolog solution but we don’t have
Prolog installed. Do you have a version that runs
on the JVM?
Thanks, Happy Customer
66 @glaforge — @paulk_asert
118. Einstein’s Riddle DSL …
From: customer@acme.org
To: Paul King
Subject: Project Request
Date: Early morning
Dear Paul,
Thanks for that version but in terms of
maintaining the rules, we would like a more
fluent expression of the rules rather than
Prolog? Any thoughts?
Thanks, Happy Customer
68 @glaforge — @paulk_asert
119. Einstein’s Riddle : Polyglot w/ DSL
// define some domain classes and objects
enum Pet { dog, cat, bird, fish, horse }
enum Color { green, white, red, blue, yellow }
enum Smoke { dunhill, blends, pallmall, prince, bluemaster }
enum Drink { water, tea, milk, coffee, beer }
enum Nationality { Norwegian, Dane, Brit, German, Swede }
dogs = dog; birds = bird; cats = cat; horses = horse
a = owner = house = the = abode = person = man = is = to =
side = next = who = different = 'ignored'
// some preliminary definitions
p = ProverFactory.prover
hintNum = 1
p.addTheory('''
persons(0, []) :‐ !.
persons(N, [(_Men,_Color,_Drink,_Smoke,_Animal)|T]) :‐ N1 is N‐1, persons(N1,T).
person(1, [H|_], H) :‐ !.
person(N, [_|T], R) :‐ N1 is N‐1, person(N1, T, R).
''')
69 @glaforge — @paulk_asert
122. Einstein’s Riddle : Polyglot w/ DSL
// now define the DSL
the man from the centre house drinks milk
the Norwegian owns the first house
the Dane drinks tea
the German smokes prince
the Swede keeps dogs // alternate ending: has a pet dog
the Brit has a red house // alternate ending: red abode
the owner of the green house drinks coffee
the owner of the yellow house smokes dunhill
the person known to smoke pallmall rears birds // alternate end: keeps birds
the man known to smoke bluemaster drinks beer
the green house is on the left side of the white house
the man known to smoke blends lives next to the one who keeps cats
the man known to keep horses lives next to the man who smokes dunhill
the man known to smoke blends lives next to the one who drinks water
the Norwegian lives next to the blue house
72 @glaforge — @paulk_asert
123. Einstein’s Riddle DSL
From: customer@acme.org
To: Paul King
Subject: Project Request
Date: Early morning
Dear Paul,
That’s great. We even get some code
completion When using an IDE. Is there
anything more we can do to get more
completion?
Thanks, Happy Customer
73 @glaforge — @paulk_asert
124. Einstein’s Riddle : Polyglot w/ DSL
// now implement DSL in terms of helper methods
def the(Nationality n) {
def ctx = [from:n]
[
drinks: { d ‐> addPairHint(ctx + [drink:d]) },
smokes: { s ‐> addPairHint(ctx + [smoke:s]) },
keeps: { p ‐> addPairHint(ctx + [pet:p]) },
...
]
}
...
the German smokes prince
the(German).smokes(prince)
n = German
ctx = [from: German]
[drinks: …,
smokes: { s ‐> addPairHint([from: German, smoke: s]) },
keeps: …,
… addPairHint([from: German, smoke:
] prince])
74 @glaforge — @paulk_asert
125. Einstein’s Riddle : Polyglot w/ DSL
• Some parts of our DSL are automatically statically inferred,
e.g. typing ‘bl’ and then asking for completion yields:
• But other parts are not known, e.g. the word ‘house’ in the
fragment below:
‘house’ is key for a Map and could be any value
75 @glaforge — @paulk_asert
127. Einstein’s Riddle DSL
From: customer@acme.org
To: Paul King
Subject: Project Request
Date: Early morning
Dear Paul,
That’s fantastic! But we have just started
standardizing on Choco as our logic solving
engine. I guess we need to start from scratch.
Let me know what you think.
Thanks, Happy Customer
77 @glaforge — @paulk_asert
128. Einstein’s Riddle : Choco w/ DSL
@GrabResolver('http://www.emn.fr/z‐info/choco‐solver/mvn/repository/')
@Grab('choco:choco:2.1.1‐SNAPSHOT')
import static choco.Choco.*
import choco.kernel.model.variables.integer.*
def m = new choco.cp.model.CPModel()
m.metaClass.plus = { m.addConstraint(it); m }
def s = new choco.cp.solver.CPSolver()
choco.Choco.metaClass.static.eq = { c, v ‐> delegate.eq(c, v.ordinal()) }
def makeEnumVar(st, arr) {
choco.Choco.makeIntVar(st, 0, arr.size()‐1, choco.Options.V_ENUM) }
pets = new IntegerVariable[num]
colors = new IntegerVariable[num]
smokes = new IntegerVariable[num]
drinks = new IntegerVariable[num]
nations = new IntegerVariable[num]
(0..<num).each { i ‐>
pets[i] = makeEnumVar("pet$i", pets)
colors[i] = makeEnumVar("color$i", colors)
smokes[i] = makeEnumVar("smoke$i", smokes)
drinks[i] = makeEnumVar("drink$i", drinks)
nations[i] = makeEnumVar("nation$i", nations)
}
...
78 @glaforge — @paulk_asert
130. Einstein’s Riddle : Choco w/ DSL
// define rules
m += all pets are different
m += all colors are different
m += all smokes are different
m += all drinks are different
m += all nations are different
m += the man from the centre house drinks milk
m += the Norwegian owns the first house
m += the Dane drinks tea
m += the German smokes prince
m += the Swede keeps dogs // alternate ending: has a pet dog
m += the Brit has a red house // alternate ending: red abode
m += the owner of the green house drinks coffee
m += the owner of the yellow house smokes dunhill
m += the person known to smoke pallmall rears birds // alt end: keeps birds
m += the man known to smoke bluemaster drinks beer
m += the green house is on the left side of the white house
m += the man known to smoke blends lives next to the one who keeps cats
m += the man known to keep horses lives next to the man who smokes dunhill
m += the man known to smoke blends lives next to the one who drinks water
m += the Norwegian lives next to the blue house
...
80 @glaforge — @paulk_asert
131. Einstein’s Riddle : Choco w/ DSL
def pretty(s, c, arr, i) {
c.values().find{ it.ordinal() == s.getVar(arr[i])?.value }
}
// invoke logic solver
s.read(m)
def more = s.solve()
while (more) {
for (i in 0..<num) {
print 'The ' + pretty(s, Nationality, nations, i)
print ' has a pet ' + pretty(s, Pet, pets, i)
print ' smokes ' + pretty(s, Smoke, smokes, i)
print ' drinks ' + pretty(s, Drink, drinks, i)
println ' and lives in a ' + pretty(s, Color, colors, i) + ' house'
}
more = s.nextSolution()
}
Solving Einstein's Riddle:
The Norwegian has a pet cat smokes dunhill drinks water and lives in a yellow house
The Dane has a pet horse smokes blends drinks tea and lives in a blue house
The Brit has a pet bird smokes pallmall drinks milk and lives in a red house
The German has a pet fish smokes prince drinks coffee and lives in a green house
The Swede has a pet dog smokes bluemaster drinks beer and lives in a white house
81 @glaforge — @paulk_asert
133. Nasa Robot
From: customer@acme.org
To: Guillaume Laforge
Subject: Project Request
Date: Early morning
Dear Guillaume,
We’ve got a contract with NASA to send a
rover on Mars. We’d need a DSL to lead
the robot on the rocky soil. Can you
help?
Thanks, Happy Customer
83 @glaforge — @paulk_asert
134. Nasa Robot
import static Direction.*
enum Direction {
left, right, forward, backward
}
class Robot {
void move(Direction dir) {
println "robot moved $dir"
}
}
Nothing special here.
def robot = new Robot()
But can we make it prettier?
robot.move left
84 @glaforge — @paulk_asert
146. Nasa Robot
Going even further?
shell.evaluate '''
move left
move right, 3.meters
move right, by: 3.meters
move right, by: 3.meters, at: 5.km/h
move right by 3.meters at 5.km/h
deploy left arm
'''
96 @glaforge — @paulk_asert
147. Domain-Specific Language Descriptors
• DSLs are nice, but what about support in my IDE?
• DSLD to the rescue
– Domain-Specific Language Descriptors
– for Eclipse STS (but GDLS concent available in IntelliJ IDEA too)
• Idea: a DSL for describing DSLs, in order to provide
– code-completion
– code navigation
– documentation hovers
97 @glaforge — @paulk_asert