Loading…

Flash Player 9 (or above) is needed to view presentations.
We have detected that you do not have it on your computer. To install it, go here.

Like this presentation? Why not share!

groovy DSLs from beginner to expert

on

  • 9,952 views

Paul King and Guillaume Laforge present Groovy.DSLs(from:beginner, to:expert) at SpringOne2GX in Chicago in October 2010

Paul King and Guillaume Laforge present Groovy.DSLs(from:beginner, to:expert) at SpringOne2GX in Chicago in October 2010

Statistics

Views

Total Views
9,952
Views on SlideShare
9,435
Embed Views
517

Actions

Likes
18
Downloads
266
Comments
0

5 Embeds 517

http://mobicon.tistory.com 439
http://dnosenko.name 63
http://clearspace.iweb.chp.co.uk 11
http://www.techgig.com 3
http://webcache.googleusercontent.com 1

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

groovy DSLs from beginner to expert groovy DSLs from beginner to expert Presentation Transcript

  • Chicago, October 19 - 22, 2010 Groovy.DSLs(from: beginner, to: expert) Paul King Guillaume Laforge @paulk_asert @glaforge Groovy Committer Groovy Project Manager ASERT, Australia SpringSource / VMWare
  • Topics Introduction • Groovy DSL Features • A Week In The Life Of DSL INC • Summary • More Info © ASERT 2006-2010 DSLs 2010 - 2
  • What is a DSL? • 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 © ASERT 2006-2010 restricted to, a particular problem domain – In contrast, general-purpose languages are created to solve problems in many domains – Somewhere between declarative data and a full blown general- purpose programming language (GPL) – AKA: fluent / human interfaces, language oriented programming, problem-oriented languages, little / mini languages, macros, business natural languages Sources: http://en.wikipedia.org/wiki/Domain-specific_language van Deursen, A., Klint, P., Visser, J.: Domain-specific languages: an annotated bibliography. ACM SIGPLAN Notices 35 (2000) 26–36
  • 1. e4 e5 2. Nf3 Nc6 3. Bb5 a6
  • L 2 U F -1 B L 2 F B -1 U L 2
  • Visual!
  • 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="@*"> <name>destroy</name> <xsl:element name="{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> Regex <widget> <class>GtkButton</class> "x.z?z{1,3}y" <name>Hello World</name> <can_focus>True</can_focus> fetchmail <label>Hello World</label> </widget> # Poll this site first each cycle. poll pop.provider.net proto pop3 </widget> </GTK-Interface> user "jsmith" with pass "secret1" is "smith" here user jones with pass "secret2" is "jjones" here with options 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 Source: Applying minilanguages: http://www.faqs.org/docs/artu/ch08s02.html DSLs 2010 - 7
  • Origins • It has always been a holy grail of computer users to be able to speak directly to our computers © ASERT 2006-2010 • Different languages have pushed the boundaries further than others – APT for numerically controlled machine tools (1957) – BNF (1959), COBOL, 4GLs – LISP & Smalltalk – Unix "little languages" DSLs 2010 - 8
  • Origins • It has always been a holy grail of computer users to be able to speak directly to our computers © ASERT 2006-2010 http://dsal.uchicago.edu/reference/gazetteer/pager.html?objectid=DS405.1.I34_V02_298.gif DSLs 2010 - 9
  • Origins: LISP & Smalltalk “In Lisp, you don’t just write your program down toward the language, you also build the language up toward your program” - Paul Graham © ASERT 2006-2010 “When [Smalltalk] is used to describe an application system, the developer extends Smalltalk, creating a domain-specific language by adding a new vocabulary of language elements ...” - Adele Goldberg DSLs 2010 - 10
  • Origins: Minilanguages... Source: The Art of Unix Programming:Taxonomy of languages: http://www.faqs.org/docs/artu/ch08s01.html # Minilanguage taxonomy %!PS-Adobe-3.0 # Base ellipses %%Creator: groff version 1.20.1 define smallellipse {ellipse width 3.0 height 1.5} ... M: ellipse width 3.0 height 1.8 fill 0.2 597.6 12 72 12 DL(increasing loopiness)297.71 8.2 Q(/etc/passwd)102.67 line from M.n to M.s dashed 94.6 Q(.ne)110.715 106.6 Q(wsrc)-.25 E(SNG)195.2 100.6 Q(re)243.8 94.6 Q D: smallellipse() with .e at M.w + (0.8, 0) (ge)-.15 E(xps)-.15 E(Glade)247.26 106.6 Q(m4)306.81 58.6 Q -1(Ya)303.43 line from D.n to D.s dashed 70.6 S(cc)1 E(Le)305.5 82.6 Q(x)-.15 E(mak)302.42 94.6 Q(e)-.1 E(XSL) I: smallellipse() with .w at M.e - (0.8, 0) 301.16 106.6 Q(T)-.92 E(pic)307.09 118.6 Q(tbl)307.92 130.6 Q(eqn)305.98 142.6 Q(fetchmail)344.715 82.6 Q -.15(aw)355.345 94.6 S(k).15 E(trof) # Arrow headings 354.84 106.6 Q(f)-.25 E(Postscript)343.875 118.6 Q(dc)412.88 94.6 Q(bc) arrow from D.w + (0.4, 0.8) to D.e + (-0.4, 0.8) 412.88 106.6 Q(Emacs Lisp)462.53 94.6 Q(Ja)465.395 106.6 Q -.25(va)-.2 G "flat to structured" "" at last arrow.c (Script).25 E(sh)529.075 94.6 Q(tcl)528.52 106.6 Q(Perl)565.065 88.6 Q ... (Python)558.95 100.6 Q(Ja)564.46 112.6 Q -.25(va)-.2 G 0 Cg EP # Interpreters %%Trailer "Emacs Lisp" "JavaScript" at 0.25 between M.e and I.e end "sh" "tcl" at 0.55 between M.e and I.e %%EOF "Perl" "Python" "Java" at 0.8 between M.e and I.e Input DSL: pic for above picture Output "DSL": Postscript for above picture DSLs 2010 - 11
  • ...Origins: Minilanguages import builder.PowerPointBuilder def name = 'minilanguages' assert new File("${name}.pic').exists() "groff -e -p ${name}.pic > ${name}.ps".execute() "gs -q -sDEVICE='ppmraw' -g2600x3500 -r300x300 -sOutputFile='-' -dBATCH ↵ –dNOPAUSE ${name}.ps | pnmcrop | ppmtogif > ${name}.gif".execute() def builder = new PowerPointBuilder() // Adapted from Erik Pragt builder.slideshow(filename: 'DSLsInGroovy_MiniLanguages.ppt') { slide(title: 'Origins: Minilanguages...') { image( origin: [0, 15], src: "${name}.gif", caption: 'Source: The Art of Unix Programming:Taxonomy...' ) textbox( origin: [5, 100], text: new File("${name}.pic").text, caption: 'Input DSL: pic for above picture' ) textbox( origin: [115, 100], text: new File("${name}.ps").text, caption: 'Output "DSL": Postscript for above picture' ) } } PowerPoint DSL for previous slide DSLs 2010 - 12
  • Subject Matter Experts, Business Analysts...
  • Developer producing LOLCODE HAI CAN HAS STDIO? I HAS A VAR IM IN YR LOOP UP VAR!!1 VISIBLE VAR IZ VAR BIGGER THAN 10? KTHXBYE IM OUTTA YR LOOP KTHXBYE
  • And in the end... nobody understands each other
  • DSL: a potential solution? • Use a more expressive language than a general purpose one • Share a common metaphor of understanding between developers 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 • Cleanly separate business logic from application code • Let business rules have their own lifecycle 16 16
  • Towards more readibility (1) 20% Copyright 2010 SpringSource. Copying, publishing or distributing without express written permission is prohibited. 17 17
  • Towards more readibility (2) 80% Copyright 2010 SpringSource. Copying, publishing or distributing without express written permission is prohibited. 18
  • Why a DSL? • Advantages: • Disadvantages: – Domain experts can – Learning cost vs. limited understand, validate, modify, applicability and often even develop DSL – Cost of designing, programs implementing & maintaining – Somewhat self-documenting DSL as well as the tools/IDEs – Enhance quality, – Attaining proper scope © ASERT 2006-2010 productivity, reliability, – Trade-offs between domain- maintainability, portability specificity and general- and reusability purpose programming – Safety; as long as the language constructs language constructs are safe – Efficiency costs any sentence written with – Proliferation of similar non- them can be considered safe standard DSLs, i.e. different but similar DSLs used within two insurance companies Source: http://en.wikipedia.org/wiki/Domain-specific_language DSLs 2010 - 19
  • DSL usage patterns... • Internal (uses a general purpose language) – AKA embedded – Can be a subset or superset – Can be generated/converted from other "language" – Numerous mechanisms to make as close to the domain as possible © ASERT 2006-2010 • External (custom language) – May involve writing your own traditional parser – Using parser combinators – Interpreting – String grepping • Other characteristics – Functional vs OO DSLs 2010 - 20
  • ...DSL usage patterns... © ASERT 2006-2010 Source: DSLs in Action, Debasish Ghosh, Manning now in MEAP DSLs 2010 - 21
  • ...DSL usage patterns © ASERT 2006-2010 Source: DSLs in Action, Debasish Ghosh, Manning now in MEAP DSLs 2010 - 22
  • DSL usage patterns (Advanced) © ASERT 2006-2010 Source: http://www.spinellis.gr/pubs/jrnl/2000-JSS-DSLPatterns/html/dslpat.html See: Diomidis Spinellis. Notable design patterns for domain specific languages. Journal of Systems and Software, 56(1):91–99, February 2001.
  • What is Groovy? • “Groovy is like a super version of Java. It can leverage Java's enterprise capabilities but also has cool productivity features like closures, DSL support, builders and dynamic typing.” © ASERT 2006-2010 Groovy = Java – boiler plate code + optional dynamic typing + closures + domain specific languages + builders + metaprogramming DSLs 2010 - 24
  • Groovy Goodies Overview • Fully object oriented • Closures: reusable and assignable pieces of code • Operators can be • GPath: efficient overloaded © ASERT 2006-2010 object navigation • Multimethods • GroovyBeans • Literal declaration for • grep and switch lists (arrays), maps, ranges and regular • Templates, builder, expressions swing, Ant, markup, XML, SQL, XML-RPC, Scriptom, Grails, tests, Mocks DSLs 2010 - 25
  • Growing Acceptance … A slow and steady start but now gaining in momentum, maturity and mindshare Now free
  • The Landscape of JVM Languages optional static types © ASERT 2006-2010 Dynamic features call for dynamic types Java bytecode calls for static types The terms “Java Virtual Machine” and “JVM” mean a Virtual Machine for the Java™ platform. DSLs 2010 - 27
  • Groovy Starter System.out.println("Hello, World!"); // supports Java syntax println 'Hello, World!' // but can remove some syntax String name = 'Guillaume' // Explicit typing/awareness println "$name, I'll get the car." // Gstring (interpolation) def longer = """${name}, the car is in the next row.""" // multi-line, implicit type assert 0.5 == 1/2 // BigDecimal equals() © ASERT 2006-2010 assert 0.1 + 0.2 == 0.3 // and arithmetic def printSize(obj) { // implicit/duck typing print obj?.size() // safe dereferencing } def pets = ['ant', 'bee', 'cat'] // native list syntax pets.each { pet -> // closure support assert pet < 'dog' // overloading '<' on String } // or: for (pet in pets)... DSLs 2010 - 28
  • A Better Java... import java.util.List; import java.util.ArrayList; class Erase { private List removeLongerThan(List strings, int length) { This code List result = new ArrayList(); for (int i = 0; i < strings.size(); i++) { is valid String s = (String) strings.get(i); if (s.length() <= length) { Java and } result.add(s); valid Groovy } return result; © ASERT 2006-2010 } public static void main(String[] args) { List names = new ArrayList(); names.add("Ted"); names.add("Fred"); names.add("Jed"); names.add("Ned"); System.out.println(names); Erase e = new Erase(); Based on an List shortNames = e.removeLongerThan(names, 3); System.out.println(shortNames.size()); example by for (int i = 0; i < shortNames.size(); i++) { Jim Weirich String s = (String) shortNames.get(i); System.out.println(s); & Ted Leung } } } DSLs 2010 - 29
  • ...A Better Java... import java.util.List; import java.util.ArrayList; class Erase { private List removeLongerThan(List strings, int length) { Do the List result = new ArrayList(); for (int i = 0; i < strings.size(); i++) { semicolons String s = (String) strings.get(i); if (s.length() <= length) { add anything? } result.add(s); And shouldn‟t } we us more return result; © ASERT 2006-2010 } modern list public static void main(String[] args) { List names = new ArrayList(); notation? names.add("Ted"); names.add("Fred"); names.add("Jed"); names.add("Ned"); Why not System.out.println(names); import common Erase e = new Erase(); List shortNames = e.removeLongerThan(names, 3); libraries? System.out.println(shortNames.size()); for (int i = 0; i < shortNames.size(); i++) { String s = (String) shortNames.get(i); System.out.println(s); } } } DSLs 2010 - 30
  • ...A Better Java... class Erase { private List removeLongerThan(List strings, int length) { List result = new ArrayList() for (String s in strings) { if (s.length() <= length) { result.add(s) } } return result } public static void main(String[] args) { List names = new ArrayList() © ASERT 2006-2010 names.add("Ted"); names.add("Fred") names.add("Jed"); names.add("Ned") System.out.println(names) Erase e = new Erase() List shortNames = e.removeLongerThan(names, 3) System.out.println(shortNames.size()) for (String s in shortNames) { System.out.println(s) } } } DSLs 2010 - 31
  • ...A Better Java... class Erase { private List removeLongerThan(List strings, int length) { List result = new ArrayList() for (String s in strings) { if (s.length() <= length) { result.add(s) Do we need } } the static types? } return result Must we always have a main public static void main(String[] args) { List names = new ArrayList() method and © ASERT 2006-2010 names.add("Ted"); names.add("Fred") names.add("Jed"); names.add("Ned") class definition? System.out.println(names) Erase e = new Erase() How about List shortNames = e.removeLongerThan(names, 3) improved System.out.println(shortNames.size()) for (String s in shortNames) { consistency? System.out.println(s) } } } DSLs 2010 - 32
  • ...A Better Java... def removeLongerThan(strings, length) { def result = new ArrayList() for (s in strings) { if (s.size() <= length) { result.add(s) } } return result } © ASERT 2006-2010 names = new ArrayList() names.add("Ted") names.add("Fred") names.add("Jed") names.add("Ned") System.out.println(names) shortNames = removeLongerThan(names, 3) System.out.println(shortNames.size()) for (s in shortNames) { System.out.println(s) } DSLs 2010 - 33
  • ...A Better Java... def removeLongerThan(strings, length) { def result = new ArrayList() for (s in strings) { if (s.size() <= length) { result.add(s) Shouldn‟t we } } have special return result notation for lists? } And special © ASERT 2006-2010 names = new ArrayList() facilities for names.add("Ted") names.add("Fred") list processing? names.add("Jed") Is „return‟ names.add("Ned") needed at end? System.out.println(names) shortNames = removeLongerThan(names, 3) System.out.println(shortNames.size()) for (s in shortNames) { System.out.println(s) } DSLs 2010 - 34
  • ...A Better Java... def removeLongerThan(strings, length) { strings.findAll{ it.size() <= length } } names = ["Ted", "Fred", "Jed", "Ned"] System.out.println(names) shortNames = removeLongerThan(names, 3) System.out.println(shortNames.size()) shortNames.each{ System.out.println(s) } © ASERT 2006-2010 DSLs 2010 - 35
  • ...A Better Java... def removeLongerThan(strings, length) { strings.findAll{ it.size() <= length } } Is the method names = ["Ted", "Fred", "Jed", "Ned"] now needed? System.out.println(names) shortNames = removeLongerThan(names, 3) Easier ways to System.out.println(shortNames.size()) use common shortNames.each{ System.out.println(s) } methods? © ASERT 2006-2010 Are brackets required here? DSLs 2010 - 36
  • ...A Better Java... names = ["Ted", "Fred", "Jed", "Ned"] println names shortNames = names.findAll{ it.size() <= 3 } println shortNames.size() shortNames.each{ println it } © ASERT 2006-2010 DSLs 2010 - 37
  • ...A Better Java names = ["Ted", "Fred", "Jed", "Ned"] println names shortNames = names.findAll{ it.size() <= 3 } println shortNames.size() shortNames.each{ println it } © ASERT 2006-2010 ["Ted", "Fred", "Jed", "Ned"] 3 Ted Jed Ned DSLs 2010 - 38
  • Grapes / Grab // Google Collections example @Grab('com.google.collections:google-collections:1.0') import com.google.common.collect.HashBiMap HashBiMap fruit = [grape:'purple', lemon:'yellow', lime:'green'] assert fruit.lemon == 'yellow' © ASERT 2006-2010 assert fruit.inverse().yellow == 'lemon' DSLs 2010 - 39
  • Malleable Syntax order to buy 200.shares of GOOG { limitPrice 500 allOrNone false at the value of { qty * unitPrice - 100 } } take 2.pills of chloroquinine after 6.hours © ASERT 2006-2010 def "length of Spock's & his friends' names"() { expect: name.size() == length where: name | length "Spock" | 5 "Kirk" | 4 "Scotty" | 6 } Groovy 1.8+ DSLs 2010 - 40
  • Real-life Groovy DSL examples • Anti-malaria drug resistance simulation • Human Resources employee skills representation • Insurance policies risk calculation engine • Loan acceptance rules engine for a financial platform • Mathematica-like lingua for nuclear safety simulations • Market data feeds evolution scenarios • and more... Copyright 2010 SpringSource. Copying, publishing or distributing without express written permission is prohibited. 41
  • DSLs within the Groovy Ecosystem • Testing DSLs – EasyB, Spock, JBehave, … • Builders – MarkupBuilder, SwingBuilder • Build Tool DSLs – AntBuilder, Gant, Gradle • Grails – BeanBuilder, Criteria, ConfigSlurper • GPars – DSL for concurrent activities • and more... Copyright 2010 SpringSource. Copying, publishing or distributing without express written permission is prohibited. 42
  • Testing DSLs: Spock... © ASERT 2006-2010 DSLs 2010 - 43
  • ...Testing DSLs: Spock... • Testing framework for Java and Groovy • Highly expressive specification language – No assertion API – No record & @Speck replay @RunWith(Sputnik) mocking API class PublisherSubscriberSpeck { – No def "events are received by all subscribers"() { superfluous def pub = new Publisher() © ASERT 2006-2010 annotations def sub1 = Mock(Subscriber) – Meaningful def sub2 = Mock(Subscriber) assert error pub.subscribers << sub1 << sub2 messages when: pub.send("event") then: 1 * sub1.receive("event") 1 * sub2.receive("event") – Extensible } – Compatible } with JUnit reportingwise DSLs 2010 - 44
  • ...Testing DSLs: Spock... import com.gargoylesoftware.htmlunit.WebClient import spock.lang.* import org.junit.runner.RunWith @Speck () @RunWith (Sputnik) class TestSimpBlogSpock { def page, subheadings, para, form, result @Unroll("When #author posts a #category blog with content '#content' it sho def "when creating a new blog entry"() { © ASERT 2006-2010 given: page = new WebClient().getPage('http://localhost:8080/postForm') form = page.getFormByName('post') when: form.getInputByName('title').setValueAttribute("$author was here (and s form.getSelectByName('category').getOptions().find { it.text == categor form.getSelectByName('author').getOptions().find { it.text == author }. form.getTextAreaByName('content').setText(content) result = form.getInputByName('btnPost').click() subheadings = result.getElementsByTagName('h3') para = result.getByXPath('//TABLE//TR/TD/P')[0] ... DSLs 2010 - 45
  • ... Testing DSLs: Spock ... then: page.titleText == 'Welcome to SimpBlog' result.getElementsByTagName('h1').item(0).textContent.matches("Post.*: $au subheadings.item(1).textContent == "Category: $category" subheadings.item(2).textContent == "Author: $author" and: para.textContent == content // Optional use of 'and:' © ASERT 2006-2010 where: author << ['Bart', 'Homer', 'Lisa'] category << ['Home', 'Work', 'Food'] content << ['foo', 'bar', 'baz'] } } DSLs 2010 - 46
  • Testing DSLs: EasyB… • Description: BDD, Rspec-like testing library narrative 'segment flown', { as_a 'frequent flyer' i_want 'to accrue rewards points for every segment I fly' so_that 'I can receive free flights for my dedication to the airline' } scenario 'segment flown', { given 'a frequent flyer with a rewards balance of 1500 points' when 'that flyer completes a segment worth 500 points' © ASERT 2006-2010 then 'that flyer has a new rewards balance of 2000 points' } scenario 'segment flown', { given 'a frequent flyer with a rewards balance of 1500 points', { flyer = new FrequentFlyer(1500) } when 'that flyer completes a segment worth 500 points', { flyer.fly(new Segment(500)) } then 'that flyer has a new rewards balance of 2000 points', { flyer.pointsBalance.shouldBe 2000 } } DSLs 2010 - 47
  • …Testing DSLs: EasyB • When run will be marked as pending – perfect for ATDD scenario "Bart posts a new blog entry", { given "we are on the create blog entry page" when "I have entered 'Bart was here' as the title" and "I have entered 'Cowabunga Dude!' into the content" and "I have selected 'Home' as the category" © ASERT 2006-2010 and "I have selected 'Bart' as the author" and "I click the 'Create Post' button" then "I expect the entry to be posted" } DSLs 2010 - 48
  • Testing DSLS: Cucumber... # language: en @newpost Feature: New Blog Post In order to create a new blog entry Bloggers should be able to select their name and category and enter text Scenario: New Posting © ASERT 2006-2010 Given we are on the create blog entry page When I have entered "Bart was here" as the title And I have entered "Cowabunga Dude!" as the content And I have selected "Home" from the "category" dropdown And I have selected "Bart" from the "author" dropdown And I click the 'Create Post' button Then I should see a heading message matching "Post.*: Bart was here.*" DSLs 2010 - 49
  • ... Testing DSLS: Cucumber... © ASERT 2006-2010 DSLs 2010 - 50
  • ...Testing DSLS: Cucumber import com.gargoylesoftware.htmlunit.WebClient this.metaClass.mixin(cuke4duke.GroovyDsl) Given ~/we are on the create blog entry page/, { -> page = new WebClient().getPage('http://localhost:8080/postForm') } When(~/I have entered "(.*)" as the title/) {String title -> form = page.getFormByName('post') form.getInputByName('title').setValueAttribute(title + ' (and so was Cucumber)') } When(~'I have entered "(.*)" as the content') {String content -> form.getTextAreaByName('content').setText(content) © ASERT 2006-2010 } When(~'I have selected "(.*)" from the "(.*)" dropdown') {String option, String name -> form.getSelectByName(name).getOptions().find { it.text == option }.setSelected(true) } When(~"I click the 'Create Post' button") { -> result = form.getInputByName('btnPost').click() } Then(~'I should see a heading message matching "(.*)"') {String pattern -> // ensureThat result.getElementsByTagName('h1').item(0).textContent.matches(pattern) assert result.getElementsByTagName('h1').item(0).textContent.matches(pattern) } DSLs 2010 - 51
  • Grails Criteria // Account is a POJO in our domain/model def c = Account.createCriteria() def results = c { like("holderFirstName", "Fred%") and { between("balance", 500, 1000) eq("branch", "London") © ASERT 2006-2010 } maxResults(10) order("holderLastName", "desc") } // source: Grails doco: 5. Object Relational Mapping (GORM): 5.4.2 Criteria DSLs 2010 - 52
  • Grails Criteria Example // Book is a POJO in our domain/model def book = Book.findByTitle("The Stand") book = Book.findByTitleLike("Harry Pot%") book = Book.findByReleaseDateBetween( firstDate, secondDate ) © ASERT 2006-2010 book = Book.findByReleaseDateGreaterThan( someDate ) book = Book.findByTitleLikeOrReleaseDateLessThan( "%Something%", someDate ) books = Book.findAllByTitleLikeAndReleaseDateGreaterThan( "%Java%", new Date()-30) // source: Grails doco: 5. Object Relational Mapping (GORM): 5.4.1 Dynamic Finders DSLs 2010 - 53
  • Grails Bean Builder Example bb.beans { marge(Person) { name = "marge" husband = { Person p -> name = "homer" age = 45 props = [overweight:true, height:"1.8m"] } © ASERT 2006-2010 children = [bart, lisa] } bart(Person) { name = "Bart" age = 11 } lisa(Person) { name = "Lisa" age = 9 } } // source: 14. Grails and Spring: 14.3 Runtime Spring with the Beans DSL DSLs 2010 - 54
  • GPars... import static org.gparallelizer.actors.pooledActors.PooledActors.* // create a new actor that prints out received messages def console = actor { loop { react {message -> println message } } } // start the actor and send it a message console.start() console.send('Message') DSLs 2010 - 55
  • ...GPars import static org.gparallelizer.dataflow.DataFlow.thread final def x = new DataFlowVariable() final def y = new DataFlowVariable() final def z = new DataFlowVariable() thread { z << ~x + ~y println "Result: ${~z}" } thread { x << 10 } No race-conditions thread { No deadlocks y << 5 No live-locks } Completely deterministic programs BEAUTIFUL code DSLs 2010 - 56
  • Topics • Introduction Groovy DSL Features • A Week In The Life Of DSL INC • Summary • More Info © ASERT 2006-2010 DSLs 2010 - 57
  • Groovy DSL Features © ASERT 2006-2010 DSLs 2010 - 58
  • Import / Import Static • Imports @Grab('com.google.collections:google-collections:1.0') import com.google.common.collect.HashBiMap as HashMap def m = new HashMap() m.key = 'value' assert m.inverse().value == 'key' © ASERT 2006-2010 • Static Imports import static java.util.Calendar.getInstance as now println now().format('yyyy/MMM/dd') What Java gives us plus aliases DSLs 2010 - 59
  • Static Imports...  Java Static Imports Discussed later:  Builders  Closures import groovy.swing.SwingXBuilder © ASERT 2006-2010 import static java.awt.Color.* import static java.lang.Math.* def swing = new SwingXBuilder() def frame = swing.frame(size: [300, 300]) { graph(plots: [ [GREEN, {value -> sin(value)}], [BLUE, {value -> cos(value)}], [RED, {value -> tan(value)}] ]) }.show() DSLs 2010 - 60
  • ...Static Imports... Java gives us this import groovy.swing.SwingXBuilder © ASERT 2006-2010 import static java.awt.Color.* import static java.lang.Math.* def swing = new SwingXBuilder() def frame = swing.frame(size: [300, 300]) { graph(plots: [ [GREEN, {value -> sin(value)}], [BLUE, {value -> cos(value)}], [RED, {value -> tan(value)}] ]) }.show() DSLs 2010 - 61
  • ...Static Imports... Java gives us this import groovy.swing.SwingXBuilder © ASERT 2006-2010 import static java.awt.Color.* import static java.lang.Math.* def swing = new SwingXBuilder() def frame = swing.frame(size: [300, 300]) { graph(plots: [ [GREEN, {value -> sin(value)}], [BLUE, {value -> cos(value)}], [RED, {value -> tan(value)}] ]) }.show() DSLs 2010 - 62
  • ...Static Imports... © ASERT 2006-2010 Source: http://www.thedoghousediaries.com/?p=1406 DSLs 2010 - 63
  • ...Static Imports Java doesn‟t give us this! import groovy.swing.SwingXBuilder import static java.lang.Math.* import static java.awt.Color.GREEN as Lime © ASERT 2006-2010 import static java.awt.Color.BLUE as Sky import static java.awt.Color.RED as Maraschino def swing = new SwingXBuilder() def frame = swing.frame(size: [300, 300]) { graph(plots: [ [Lime, {value -> sin(value)}], [Sky, {value -> cos(value)}], [Maraschino, {value -> tan(value)}] ]) }.show() DSLs 2010 - 64
  • Literal Syntax Conventions • Lists – Special syntax for list literals – Additional common methods (operator overloading) def list = [3, new Date(), 'Jan'] assert list + list == list * 2 • Maps © ASERT 2006-2010 – Special syntax for map literals – Additional common methods def map = [a: 1, b: 2] assert map['a'] == 1 && map.b == 2 • Ranges – Special syntax for various kinds of ranges def letters = 'a'..'z' def numbers = 0..<10 DSLs 2010 - 65
  • Literal Syntax Conventions in DSLs import static java.util.Calendar.getInstance as getNow import static java.util.Calendar.* def discount = [normal:0, silver:5, gold:10] def roomrate = [weekday:150, weekend:95] for (level in ['normal', 'silver', 'gold']) { def multiplier = 1 - discount[level] / 100 print 'Your room rate is: ' © ASERT 2006-2010 if (now[DAY_OF_WEEK] in MONDAY..FRIDAY) println roomrate.weekday * multiplier else println roomrate.weekend * multiplier }  Literal list & map syntax  Ranges Your room rate is: 150 Also Your room rate is: 142.50  Static import aliases Your room rate is: 135.0  BigDecimal arithmetic DSLs 2010 - 66
  • Compact Syntax... • Java public class BuySharesJava { public static void buyShares(int qty, String name) { // business logic here ... System.out.println("buying " + qty + " shares of " + name); } © ASERT 2006-2010 public static void main(String[] args) { buyShares(3, "BHP"); }  Script syntax  Conventions for visibility }  GDK methods: println • Groovy  Implicit typing  GString interpolation def buyShares(qty, name) {  Optional brackets & „;‟ // business logic here ... println "buying $qty shares of $name" } buyShares 3, 'BHP' DSLs 2010 - 67
  • ...Compact Syntax... • Java  Script syntax, Named params, Extra imports  Conventions for visibility, GDK methods import java.util.Date;  Implicit typing, Multi-line GString import java.util.HashMap;  Elvis operator, Optional brackets & „;‟ import java.util.Map; public class ProcessCustomerJava { public static void printDetails(Map<String, String> cust) { System.out.println("Details as at: " + new Date()); String first = cust.get("first"); System.out.println("First name: " + first); © ASERT 2006-2010 String last = cust.get("last"); System.out.println("Last name: " + (last != null ? last : "unknown")); } public static void main(String[] args) { Map<String, String> details = new HashMap<String, String>(); details.put("first", "John"); details.put("last", "Smith"); printDetails(details); } } def printDetails(cust) { println """Details as at: ${new Date()} First name: $cust.first • Groovy Last name: ${cust.last ?: 'unknown'}""" } printDetails first: 'John', last: 'Smith' DSLs 2010 - 68
  • ...Compact Syntax • Java import java.util.Date; public class ProcessStrongCustomerJava { public static void printDetails(CustomerJ cust) { System.out.println("Details as at: " + new Date()); String first = cust.getFirst(); System.out.println("First name: " + first); • Groovy String last = cust.getLast(); System.out.println("Last name: " + (last != null ? last : "unknown")); } public static void main(String[] args) { CustomerJ c = new CustomerJ(); class CustomerG { String first, last } © ASERT 2006-2010 c.setFirst("John"); c.setLast("Smith"); def printDetails(cust) { printDetails(c); } println """Details as at: ${new Date()} } First name: $cust.first class CustomerJ { Last name: ${cust.last ?: 'unknown'}""" private String first; private String last; } public String getFirst() { return first; printDetails new CustomerG(first: 'John', } last: 'Smith') public void setFirst(String first) { this.first = first; } Plus:  Named params for constructors public String getLast() { return last;  JavaBean conventions } public void setLast(String last) { this.last = last;  Leverage Duck Typing } } DSLs 2010 - 69
  • Using With Example letters = ['a', 'b', 'c'] range = 'b'..'d' letters.with { add 'd' Just normal methods for ArrayList here remove 'a' } © ASERT 2006-2010 assert letters == range map = [a:10, b:4, c:7] map.with { assert (a + b) / c == 2 } DSLs 2010 - 70
  • Closures... • Traditional mainstream languages – Data can be stored in variables, passed around, combined in structured ways to form more complex data; code stays put where it is defined • Languages supporting closures – Data and code can be stored in variables, passed © ASERT 2006-2010 around, combined in structured ways to form more complex algorithms and data; functional coding style doubleNum = { num -> num * 2 } println doubleNum(3) // => 6 processThenPrint = { num, closure -> num = closure(num); println "num is $num" } processThenPrint(3, doubleNum) // => num is 6 processThenPrint(10) { it / 2 } // => num is 5 DSLs 2010 - 71
  • ...Closures... int myConst = 4 def multiplier = { number -> number * myConst } assert multiplier(10) == 40 Name © ASERT 2006-2010 Closure other = { it + myConst } assert other.call(10) == 14 def twice = { it * 2 } // anonymous functions assert twice(10) == 20 // implemented as Closures DSLs 2010 - 72
  • ...Closures... int myConst = 4 def multiplier = { number -> number * myConst } assert multiplier(10) == 40 Code © ASERT 2006-2010 Closure other = { it + myConst } assert other.call(10) == 14 def twice = { it * 2 } // anonymous functions assert twice(10) == 20 // implemented as Closures DSLs 2010 - 73
  • ...Closures... Parameter(s) int myConst = 4 def multiplier = { number -> number * myConst } assert multiplier(10) == 40 Default parameter © ASERT 2006-2010 Closure other = { it + myConst } assert other.call(10) == 14 def twice = { it * 2 } // anonymous functions assert twice(10) == 20 // implemented as Closures DSLs 2010 - 74
  • ...Closures... int myConst = 4 def multiplier = { number -> number * myConst } assert multiplier(10) == 40 Call closure © ASERT 2006-2010 Closure other = { it + myConst } assert other.call(10) == 14 Alternative syntax def twice = { it * 2 } // anonymous functions assert twice(10) == 20 // implemented as Closures DSLs 2010 - 75
  • ...Closures... int myConst = 4 def multiplier = { number -> number * myConst } assert multiplier(10) == 40 Free variable © ASERT 2006-2010 Closure other = { it + myConst } assert other.call(10) == 14 def twice = { it * 2 } // anonymous functions assert twice(10) == 20 // implemented as Closures DSLs 2010 - 76
  • ...Closures... Bound to environment/context when called int myConst = 4 def multiplier = { number -> number * myConst } assert multiplier(10) == 40 © ASERT 2006-2010 Closure other = { it + myConst } assert other.call(10) == 14 def twice = { it * 2 } // anonymous functions assert twice(10) == 20 // implemented as Closures DSLs 2010 - 77
  • Closures in DSLs... Nothing special here. import static java.util.Calendar.* Just a list of maps. def cart1 = [ [qty: 3, unitPrice: 50.0, gift: true, name: 'Groovy in Action Book'], [qty: 1, unitPrice: 40.0, gift: false, name: 'Grails in Action eBook'], [qty: 2, unitPrice: 25.0, gift: true, © ASERT 2006-2010 name: 'Avatar DVD'] ] def cart2 = [ [qty: 1, unitPrice: 50.0, gift: true, name: 'Groovy in Action Book'], [qty: 2, unitPrice: 30.0, gift: false, name: 'Avatar 3D DVD'] ] // ... DSLs 2010 - 78
  • ... Closures in DSLs... // ... def noDiscount = { it.unitPrice * it.qty } def tenPercentOffAll = { it.unitPrice * it.qty * 0.9 } def tenPercentOffGifts = { it.with { unitPrice * qty * (gift ? 0.9 : 1.0) } } def tenPercentOffOverForty = { it.with { unitPrice * qty * (unitPrice > 40 ? 0.9 : 1.0) © ASERT 2006-2010 } } def tenPercentOffBooks = { it.with { unitPrice * qty * (name.contains('Book') ? 0.9 : 1.0) } } def tenPercentOffDVDs = { it.with { unitPrice * qty * (name.contains('DVD') ? 0.9 : 1.0) } } def buyThreeGetOneFree = { it.with { unitPrice * (qty % 3 + 2 * qty.intdiv(3)) } } // ... Closure Name DSLs 2010 - 79
  • ... Closures in DSLs... // ... def noDiscount = { it.unitPrice * it.qty } def tenPercentOffAll = { it.unitPrice * it.qty * 0.9 } def tenPercentOffGifts = { it.with { unitPrice * qty * (gift ? 0.9 : 1.0) } } def tenPercentOffOverForty = { it.with { unitPrice * qty * (unitPrice > 40 ? 0.9 : 1.0) © ASERT 2006-2010 } } def tenPercentOffBooks = { it.with { unitPrice * qty * (name.contains('Book') ? 0.9 : 1.0) } } def tenPercentOffDVDs = { it.with { unitPrice * qty * (name.contains('DVD') ? 0.9 : 1.0) } } def buyThreeGetOneFree = { it.with { unitPrice * (qty % 3 + 2 * qty.intdiv(3)) } } // ... Closure Code DSLs 2010 - 80
  • ... Closures in DSLs // ... def specials = [noDiscount, tenPercentOffAll, tenPercentOffGifts, tenPercentOffOverForty, 1 240.00 tenPercentOffBooks, tenPercentOffDVDs, buyThreeGetOneFree] 2 216.00 3 220.00 [ 4 225.00 SUNDAY..SATURDAY, 5 221.00 © ASERT 2006-2010 [cart1, cart2] 6 235.00 ].combinations().each { day, cart -> 7 190.00 printf "%d %2.2fn", day, cart.sum(specials[day-1]) 1 110.00 } 2 99.00 3 105.00 4 105.00 5 105.00 6 104.00 7 110.00 DSLs 2010 - 81
  • Operator Overloading Example • Java BigDecimal a = new BigDecimal(3.5d); BigDecimal b = new BigDecimal(4.0d); assert a.multiply(b).compareTo(new BigDecimal(14.0d)) == 0; assert a.multiply(b).equals(new BigDecimal(14.0d).setScale(1)); • Groovy © ASERT 2006-2010 def c = 3.5, d = 4.0 assert c * d == 14.0 DSLs 2010 - 82
  • Groovy Lab Example // require GroovyLab import static org.math.array.Matrix.* import static org.math.plot.Plot.* def A = rand(10,3) // random Matrix of 10 rows and 3 columns def B = fill(10,3,1.0) // one Matrix of 10 rows and 3 columns def C = A + B // support for matrix addition with "+" or "-" def D = A - 2.0 // support for number addition with "+" or "-" def E = A * B © ASERT 2006-2010 // support for matrix multiplication or division def F = rand(3,3) def G = F**(-1) // support for matrix power (with integers only) println A // display Matrix content plot("A",A,"SCATTER") // plot Matrix values as ScatterPlot def M = rand(5,5) + id(5) // Eigenvalues decomposition println "M=n" + M println "V=n" + V(M) println "D=n" + D(M) println "M~n" + (V(M) * D(M) * V(M)**(-1)) DSLs 2010 - 83
  • Better Control Structures: Switch Poker… hand1 hand2 8C TS KC 9H 4S 7D 2S 5D 3S AC suits = 'SHDC' ranks = '23456789TJQKA' suit = { String card -> suits.indexOf(card[1]) } rank = { String card -> ranks.indexOf(card[0]) } rankSizes = { List cards -> cards.groupBy(rank).collect{ k, v -> v.size() }.sort() } © ASERT 2006-2010 rankValues = { List cards -> cards.collect{ rank(it) }.sort() } // ... println rankSizes(["7S", "7H", "2H", "7D", "AH"]) // => [1, 1, 3] DSLs 2010 - 84
  • …Better Control Structures: Switch Poker… // ... flush = { List cards -> cards.groupBy(suit).size() == 1 } straight = { def v = rankValues(it); v == v[0]..v[0]+4 } straightFlush = { List cards -> straight(cards) && flush(cards) © ASERT 2006-2010 } fourOfAKind = { List cards -> rankSizes(cards) == [1, 4] } fullHouse = { List cards -> rankSizes(cards) == [2, 3] } threeOfAKind = { List cards -> rankSizes(cards) == [1, 1, 3] } twoPair = { List cards -> rankSizes(cards) == [1, 2, 2] } pair = { List cards -> rankSizes(cards) == [1, 1, 1, 2] } // ... DSLs 2010 - 85
  • … Better Control Structures: Switch Poker // ... def rankHand(List cards) { switch (cards) { case straightFlush : return 9 case fourOfAKind : return 8 case fullHouse : return 7 case flush : return 6 © ASERT 2006-2010 case straight : return 5 case threeOfAKind : return 4 case twoPair : return 3 case pair : return 2 default : return 1 } } // ... DSLs 2010 - 86
  • Type Transformation Example class InventoryItem { def weight, name InventoryItem(Map m) { this.weight = m.weight; this.name = m.name } InventoryItem(weight, name) { this.weight = weight; this.name = name } InventoryItem(String s) { © ASERT 2006-2010 s.find(/weight=(d*)/) { all, w -> this.weight = w } s.find(/name=(.*)/) { all, n -> this.name = n } } } def room = [:] def gold = [weight:50, name:'Gold'] as InventoryItem def emerald = [10, 'Emerald'] as InventoryItem def dagger = ['weight=5, name=Dagger'] as InventoryItem room.contents = [gold, emerald, dagger] room.contents.each{ println it.dump() } DSLs 2010 - 87
  • Groovy DSL Features © ASERT 2006-2010 DSLs 2010 - 88
  • Metaprogramming • Runtime Metaprogramming – Categories including DGM – invokeMethod, methodMissing, getProperty – GroovyInterceptable – ExpandoMetaClass © ASERT 2006-2010 DSLs 2010 - 89
  • Builders… • MarkupBuilder <html> import groovy.xml.* <head> def page = new MarkupBuilder() <title>Hello</title> page.html { </head> head { title 'Hello' } <body> body { © ASERT 2006-2010 <ul> ul { <li>world 1</li> for (count in 1..5) { <li>world 2</li> li "world $count" <li>world 3</li> } } } } <li>world 4</li> <li>world 5</li> </ul> </body> </html> DSLs 2010 - 90
  • ...Builders… • GraphicsBuilder def colors = ['red','darkOrange','blue','darkGreen'] (0..3).each { index -> star( cx: 50 + (index*110), cy: 50, or: 40, ir: 15, borderColor: 'black', count: 2+index, fill: colors[index] ) star( cx: 50 + (index*110), cy: 140, or: 40, ir: 15, borderColor: 'black‘, count: 7+index, fill: colors[index] ) } © ASERT 2006-2010 DSLs 2010 - 91
  • …Builders new AntBuilder().with { echo(file:'Temp.java', ''' class Temp { public static void main(String[] args) { System.out.println("Hello"); } } ''') © ASERT 2006-2010 javac(srcdir:'.', includes:'Temp.java', fork:'true') java(classpath:'.', classname:'Temp', fork:'true') echo('Done') } // => // [javac] Compiling 1 source file // [java] Hello // [echo] Done DSLs 2010 - 92
  • A builder for HR Copyright 2010 SpringSource. Copying, publishing or distributing without express written permission is prohibited. 93
  • ExpandoMetaClass… import static java.lang.Character.isUpperCase String.metaClass.swapCase = { delegate.collect { ch -> isUpperCase(ch as char) ? ch.toLowerCase() : ch.toUpperCase() }.join() } © ASERT 2006-2010 println new Date().toString().swapCase() // => sUN nOV 09 17:30:06 est 2008 List.metaClass.sizeDoubled = {-> delegate.size() * 2 } LinkedList list = [] list << 1 list << 2 assert 4 == list.sizeDoubled() DSLs 2010 - 94
  • …ExpandoMetaClass class Person { String name } class MortgageLender { def borrowMoney() { "buy house" } © ASERT 2006-2010 } def lender = new MortgageLender() Person.metaClass.buyHouse = lender.&borrowMoney def p = new Person() assert "buy house" == p.buyHouse() DSLs 2010 - 95
  • Adding your own control structures • Thread enhancement ROCK! . 25 import java.util.concurrent.locks.ReentrantLock . 33 import static System.currentTimeMillis as now . 34 def startTime = now() . 35 ReentrantLock.metaClass.withLock = { critical -> . 36 lock() .. 131 try { critical() } .. 134 finally { unlock() } .. 137 } def lock = new ReentrantLock() .. 138 def worker = { threadNum -> .. 139 4.times { count -> ... 232 lock.withLock { ... 234 print " " * threadNum ... 237 print "." * (count + 1) ... 238 println " ${now() - startTime}" ... 239 } .... 334 Thread.sleep 100 .... 336 } } .... 337 5.times { Thread.start worker.curry(it) } .... 338 println "ROCK!" .... 339 Source: http://chrisbroadfoot.id.au/articles/2008/08/06/groovy-threads DSLs 2010 - 96
  • EMC DSL… © ASERT 2006-2010 DSLs 2010 - 97
  • ...EMC Neo4j DSL… @GrabResolver(name= 'neo4j-public-repo', root= 'http://m2.neo4j.org') @Grab('org.neo4j:neo4j-kernel:1.1.1') import org.neo4j.kernel.EmbeddedGraphDatabase import org.neo4j.graphdb.* // an enum helper enum MyRelationships implements RelationshipType { knows } // some optional syntactic sugar using EMC DSL Node.metaClass { © ASERT 2006-2010 propertyMissing { String name, val -> delegate.setProperty(name, val) } propertyMissing { String name -> delegate.getProperty(name) } methodMissing { String name, args -> delegate.createRelationshipTo(args[0], MyRelationships."$name") } } Relationship.metaClass { propertyMissing { String name, val -> delegate.setProperty(name, val) } propertyMissing { String name -> delegate.getProperty(name) } } // ... DSLs 2010 - 98
  • …EMC Neo4j DSL // ... def graphDb = new EmbeddedGraphDatabase("graphdb") def tx = graphDb.beginTx() def firstNode, secondNode, relationship try { firstNode = graphDb.createNode() secondNode = graphDb.createNode() relationship = firstNode.knows(secondNode) firstNode.message = "Hello," secondNode.message = "world!" © ASERT 2006-2010 relationship.message = "brave Neo4j" tx.success() } finally { tx.finish() println "$firstNode.message $relationship.message $secondNode.message" // => Hello, brave Neo4j world! graphDb.shutdown() } DSLs 2010 - 99
  • Game example... // Trying out the game DSL idea by Sten Anderson from: // http://blogs.citytechinc.com/sanderson/?p=92 class GameUtils { static VOWELS = ['a', 'e', 'i', 'o', 'u'] static listItems(things) { def result = '' things.eachWithIndex{ thing, index -> if (index > 0) { © ASERT 2006-2010 if (index == things.size() - 1) result += ' and ' else if (index < things.size() - 1) result += ', ' } result += "${thing.toLowerCase()[0] in VOWELS ? 'an' : 'a'} $thing } result ?: 'nothing' } } import static GameUtils.* ... DSLs 2010 - 100
  • ...Game example... ... class Room { def description def contents = [] } ... © ASERT 2006-2010 DSLs 2010 - 101
  • ...Game example... ... class Player { def currentRoom def inventory = [] void look() { println "You are in ${currentRoom?.description?:'the void'} which contains ${listItems(currentRoom?.contents)}" } © ASERT 2006-2010 void inv() { println "You are holding ${listItems(inventory)}" } void take(item) { if (currentRoom?.contents?.remove(item)) { inventory << item println "You took the $item" } else { println "I see no $item here" } } ... DSLs 2010 - 102
  • ...Game example... ... void drop(item) { if (inventory?.remove(item)) { currentRoom?.contents << item println "You dropped the $item" } else { println "You don't have the $item" } © ASERT 2006-2010 } def propertyMissing(String name) { if (metaClass.respondsTo(this, name)) { this."$name"() } name } } ... DSLs 2010 - 103
  • ...Game example... ... Room plainRoom = new Room(description:'a plain white room', contents:['dagger', 'emerald', 'key']) Player player = new Player(currentRoom:plainRoom) player.with { inv look take dagger © ASERT 2006-2010 inv look take emerald inv look take key drop emerald inv look } assert player.inventory == ['dagger', 'key'] ... DSLs 2010 - 104
  • ...Game example ... // now try some error conditions plainRoom.description = null player.with { drop gold take gold drop emerald © ASERT 2006-2010 take emerald take emerald look } DSLs 2010 - 105
  • GEP3 example... Object.metaClass.please = { clos -> clos(delegate) } Object.metaClass.the = { clos -> delegate[1](clos(delegate[0])) } show = { thing -> [thing, { println it }] } square_root = { Math.sqrt(it) } © ASERT 2006-2010 given = { it } given 100 please show the square_root // ==> 10.0 DSLs 2010 - 106
  • ...GEP3 example... Object.metaClass.of = { delegate[0](delegate[1](it)) } Object.metaClass.the = { clos -> [delegate[0], clos] } show = [{ println it }] © ASERT 2006-2010 square_root = { Math.sqrt(it) } please = { it } please show the square_root of 100 // ==> 10.0 DSLs 2010 - 107
  • ...GEP3 example... show = { println it } square_root = { Math.sqrt(it) } def please(action) { [the: { what -> [of: { n -> action(what(n)) }] © ASERT 2006-2010 }] } please show the square_root of 100 // ==> 10.0 Inspiration for this example came from … DSLs 2010 - 108
  • ...GEP3 example... // Japanese DSL using GEP3 rules Object.metaClass.を = Object.metaClass.の = { clos -> clos(delegate) } まず = { it } 表示する = { println it } © ASERT 2006-2010 平方根 = { Math.sqrt(it) } まず 100 の 平方根 を 表示する // First, show the square root of 100 // => 10.0 // source: http://d.hatena.ne.jp/uehaj/20100919/1284906117 // http://groovyconsole.appspot.com/edit/241001 DSLs 2010 - 109
  • ...GEP3 example © ASERT 2006-2010 // source: http://d.hatena.ne.jp/uehaj/20100919/1284906117 // http://groovyconsole.appspot.com/edit/241001 DSLs 2010 - 110
  • Medical Prescription DSL… class Drug { String name String toString() { name } } class Measure { Number number String unit, units String toString() { number == 1 ? "1 $unit" : "$number $units" } © ASERT 2006-2010 } class Quantity extends Measure {} class Duration extends Measure {} // ... http://groovyconsole.appspot.com/script/240001 by glaforge/Mihai Cazacu DSLs 2010 - 111
  • …Medical Prescription DSL… // ... // ---- DSL implementation ------------------------------ Integer.metaClass.getHour = {-> 1.hours } Integer.metaClass.getPill = {-> 1.pills } Integer.metaClass.getHours = {-> new Duration(number: delegate, unit: 'hour', units: 'hours') } Integer.metaClass.getPills = {-> new Quantity(number: delegate, unit: 'pill', units: 'pills') } // use the script binding for storing new drugs available as variables © ASERT 2006-2010 binding = new Binding() { def getVariable(String drug) { new Drug(name: drug) } } take = {Quantity quantity -> ['of': {Drug drug -> ['after': {Duration duration -> println "Take $quantity of $drug afer $duration" }] }] } // ... DSLs 2010 - 112
  • …Medical Prescription DSL // ... // ---- DSL ------------------------------- take 2.pills of chloroquinine after 6.hours © ASERT 2006-2010 http://groovyconsole.appspot.com/script/240001 by glaforge/Mihai Cazacu DSLs 2010 - 113
  • Groovy DSL Features © ASERT 2006-2010 Transformation DSLs 2010 - 114
  • AST Transformations • With metaprogramming, Groovy‟s able to modify the behaviour of programs... at runtime • AST Transformations provide a compile- time approach to modify programs – AST: Abstract Syntax Tree – Ability to change what‟s being compiled at compile- time! • No runtime impact! • Lets you change the semantics of your programs! • Nice way of implementing patterns and removing boiler-plate technical code • Two kinds of transformations: global & local DSLs 2010 - 115
  • Better Design Patterns: Immutable... • Java Immutable Class – As per Joshua Bloch // ... @Override Effective Java public boolean equals(Object obj) { if (this == obj) public final class Punter { return true; private final String first; if (obj == null) private final String last; return false; if (getClass() != obj.getClass()) public String getFirst() { return false; return first; Punter other = (Punter) obj; } if (first == null) { if (other.first != null) © ASERT 2006-2010 public String getLast() { return false; return last; } else if (!first.equals(other.first)) } return false; if (last == null) { @Override if (other.last != null) public int hashCode() { return false; final int prime = 31; } else if (!last.equals(other.last)) int result = 1; return false; result = prime * result + ((first == null) return true; ? 0 : first.hashCode()); } result = prime * result + ((last == null) ? 0 : last.hashCode()); @Override return result; public String toString() { } return "Punter(first:" + first + ", last:" + last + ")"; public Punter(String first, String last) { } this.first = first; this.last = last; } } // ... DSLs 2010 - 116
  • ...Better Design Patterns: Immutable... • Java Immutable Class boilerplate – As per Joshua Bloch // ... @Override Effective Java public boolean equals(Object obj) { if (this == obj) public final class Punter { return true; private final String first; if (obj == null) private final String last; return false; if (getClass() != obj.getClass()) public String getFirst() { return false; return first; Punter other = (Punter) obj; } if (first == null) { if (other.first != null) © ASERT 2006-2010 public String getLast() { return false; return last; } else if (!first.equals(other.first)) } return false; if (last == null) { @Override if (other.last != null) public int hashCode() { return false; final int prime = 31; } else if (!last.equals(other.last)) int result = 1; return false; result = prime * result + ((first == null) return true; ? 0 : first.hashCode()); } result = prime * result + ((last == null) ? 0 : last.hashCode()); @Override return result; public String toString() { } return "Punter(first:" + first + ", last:" + last + ")"; public Punter(String first, String last) { } this.first = first; this.last = last; } } // ... DSLs 2010 - 117
  • ...Better Design Patterns: Immutable @Immutable class Punter { String first, last } © ASERT 2006-2010 DSLs 2010 - 118
  • Spock def "length of Spock's & his friends' names"() { expect: name.size() == length where: name | length "Spock" | 5 © ASERT 2006-2010 "Kirk" | 4 "Scotty" | 6 } DSLs 2010 - 119
  • AST Builder • Numerous approaches, still evolving. “From code” approach: def result = new AstBuilder().buildFromCode { println "Hello World" } • Produces: BlockStatement -> ReturnStatement -> MethodCallExpression -> VariableExpression("this") -> ConstantExpression("println") -> ArgumentListExpression -> ConstantExpression("Hello World") DSLs 2010 - 120
  • EasyB… • Description: BDD, Rspec-like testing library narrative 'segment flown', { as_a 'frequent flyer' i_want 'to accrue rewards points for every segment I fly' so_that 'I can receive free flights for my dedication to the airline' } scenario 'segment flown', { given 'a frequent flyer with a rewards balance of 1500 points' when 'that flyer completes a segment worth 500 points' © ASERT 2006-2010 then 'that flyer has a new rewards balance of 2000 points' } scenario 'segment flown', { given 'a frequent flyer with a rewards balance of 1500 points', { flyer = new FrequentFlyer(1500) } when 'that flyer completes a segment worth 500 points', { flyer.fly(new Segment(500)) } then 'that flyer has a new rewards balance of 2000 points', { flyer.pointsBalance.shouldBe 2000 } } DSLs 2010 - 121
  • …EasyB • Description: BDD, Rspec-like testing library examples "The number #{number}' should be converted to #{romanNumerals}", { number = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] romanNumerals = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"] } scenario "Converting #number into the roman numeral #romanNumerals", { given "the number #number", { theNumber = number © ASERT 2006-2010 } when "the system converts this number to the roman numeral equivalent", { theConvertedNumber = RomanNumerals.fromInteger(theNumber) } then "the result should be #romanNumerals", { theConvertedNumber.shouldBe romanNumerals } } Source: http://www.wakaleo.com/blog/285-example-driven-testing-with-easyb DSLs 2010 - 122
  • EasyB Preprocessing... • EasyB supports either of these: given "some data", { println '... setting expectations' } given ("some data") { println '... setting expectations' } • But we would like this: given "some data" { println '... setting expectations' } But also consider Command Expressions (GEP-3) for Groovy1.8+. Source: http://hamletdarcy.blogspot.com/2010/02/groovy-antlr-plugins-for-better-dsls.html DSLs 2010 - 123
  • ...EasyB Preprocessing... String addCommas(text) { def pattern = ~/(.*)(given|when|then) "([^"]*(.[^"]*)*)" {(.*)/ def replacement = /$1$2 "$3", {$4/ (text =~ pattern).replaceAll(replacement) } Adds the comma in where required before Groovy sees it. Source: http://hamletdarcy.blogspot.com/2010/02/groovy-antlr-plugins-for-better-dsls.html DSLs 2010 - 124
  • ...EasyB Preprocessing class SourceModifierParserPlugin extends AntlrParserPlugin { Reduction parseCST(SourceUnit sourceUnit, Reader reader) throws CompilationFailedException { def text = addCommas(reader.text) StringReader stringReader = new StringReader(text) super.parseCST(sourceUnit, stringReader) } } def parserPluginFactory = new ParserPluginFactory() { ParserPlugin createParserPlugin() { new SourceModifierParserPlugin() } } def conf = new CompilerConfiguration(pluginFactory: parserPluginFactory) def binding = ... def shell = new GroovyShell(binding, conf) But use with restraint as error messages will be with respect to converted source code not original. Source: http://hamletdarcy.blogspot.com/2010/02/groovy-antlr-plugins-for-better-dsls.html DSLs 2010 - 125
  • GParsec... • Miniature parsers def isLetter = { ch -> return Character.isLetter(ch) } def isDigit = { ch -> return Character.isDigit(ch) } def letter = satisfyC(isLetter) def digit = satisfyC(isDigit) • Parser Combinators def letterAndDigit = seqC(letter, digit) assert letterAndDigit('a123###') == ['a', '1'] def identifier = seqC(identifierStart, noneOrMoreC(identifierRest)) DSLs 2010 - 126
  • ...GParsec... • BNF variableDeclarator : identifier ( '=' expression )? variableDefinitions : variableDeclarator ( ',' variableDeclarator)* • Groovy definitions def variableDeclarator = seqC(identifier, optionalC(seqC(assign, expression))) def variableDefinitions = seqC(variableDeclarator, noneOrMoreC(seqC(comma, variableDeclarator))) DSLs 2010 - 127
  • ...GParsec • satisfyC: combinator consumes a single input when its predicate succeeds • altC (alt3C, altCs): is the choice combinator. Given two parsers it only looks at its second alternative if the first has not consumed any input - regardless of the final value • seqC (seq3C, seqCs): is the sequencing combinator. It runs two parsers in succession and if successful, returns the result of the two parsers • noneOrMoreC, oneOrMoreC: applies a parser zero or more times to an input stream. The result from each application of the parser are returned in a list • optionalC: The combinator optionalC may succeed in parsing some input. It always returns success. DSLs 2010 - 128
  • Using External Parsers import static com.mdimension.jchronic.Chronic.parse def span = parse("tomorrow at 5pm") println span.endCalendar.format('yyyy-MM-dd HH:mm') // => 2010-05-19 17:00 © ASERT 2006-2010 DSLs 2010 - 129
  • Topics • Introduction • Groovy DSL Features A Week In The Life Of DSL INC • Summary • More Info © ASERT 2006-2010 DSLs 2010 - 130
  • MONDAY © ASERT 2006-2010 DSLs 2010 - 131
  • 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. © ASERT 2006-2010 Thanks, Happy Customer DSLs 2010 - 132
  • …Currency DSL... enum Coin { penny(1), nickel(5), dime(10), quarter(25) Coin(int value) { this.value = value } int value } © ASERT 2006-2010 import static Coin.* assert 2 * quarter.value + 1 * nickel.value + 2 * penny.value == 57 DSLs 2010 - 133
  • …Currency DSL… From: customer@acme.org To: Guillaume Subject: Project Request Dear Guillaume, That works fairly well but can we get rid of the © ASERT 2006-2010 ‘.value’ part of those expressions. They are confusing our users. Thanks, Happy Customer DSLs 2010 - 134
  • ...Currency DSL... class CoinMath { static multiply(Integer self, Coin c) { self * c.value } } use (CoinMath) { © ASERT 2006-2010 assert 2 * quarter + 1 * nickel + 2 * penny == 57 } // EMC equivalent Integer.metaClass.multiply = { Coin c -> delegate * c.value } assert 2 * quarter + 1 * nickel + 2 * penny == 57 DSLs 2010 - 135
  • …Currency DSL… From: customer@acme.org To: Guillaume Subject: Project Request Dear Guillaume, Much better but our users are sometimes using © ASERT 2006-2010 plural as well as singular expressions. Is there anything that you can do about that? Thanks, Happy Customer DSLs 2010 - 136
  • ... Currency DSL class CoinValues { static get(Integer self, String name) { self * Coin."${singular(name)}".value } static singular(String val) { val.endsWith('ies') ? val[0..-4] + 'y' : val.endsWith('s') ? val[0..-2] : val } } use (CoinValues) { assert 2.quarters + 1.nickel + 2.pennies == 57 © ASERT 2006-2010 } // EMC equivalent Integer.metaClass.getProperty = { String name -> def mp = Integer.metaClass.getMetaProperty(name) if (mp) return mp.getProperty(delegate) def singular = name.endsWith('ies') ? name[0..-4] + 'y' : name.endsWith('s') ? name[0..-2] : name delegate * Coin."$singular".value } assert 2.quarters + 1.nickel + 2.pennies == 57 DSLs 2010 - 137
  • TUESDAY © ASERT 2006-2010 http://www.google.com/imgres?imgurl=http://i733.photobucket.com/albums/ww336/snake1953/Day_of_Week/Tue/HappyTuesda DSLs 2010 - 138
  • 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 © ASERT 2006-2010 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. DSLs 2010 - 139
  • …Business logic DSL… • Options – Embedded Groovy – Regex parser – Antlr parser – Split / StringTokenizer – Parser combinators © ASERT 2006-2010 • … Many options, details not important … DSLs 2010 - 140
  • … 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 © ASERT 2006-2010 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. DSLs 2010 - 141
  • … Business logic DSL … def shell = new GroovyShell() shell.evaluate(''' price = 3 quantity = 10 total = price * quantity assert total == 30 ''') © ASERT 2006-2010 DSLs 2010 - 142
  • … 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 © ASERT 2006-2010 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. DSLs 2010 - 143
  • … 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) © ASERT 2006-2010 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.") } // ... } Please also see the ArithmeticShell which is included under examples in the Groovy distribution DSLs 2010 - 144
  • … Business logic DSL def shell = new CustomerShell() shell.evaluate(''' price = 3 quantity = 10 total = price * quantity assert total == 30 ''') © ASERT 2006-2010 DSLs 2010 - 145
  • WEDNESDAY © ASERT 2006-2010 http://www.crystalscomments.com/1/days/wednesday/animals/006.jpg DSLs 2010 - 146
  • 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 © ASERT 2006-2010 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/ DSLs 2010 - 147
  • … Stock Exchange Order DSL… // ----- Implementation of the Fluent API ----- enum Action { Buy, Sell } class Order { println new Order() def security .sell(150, "IBM") def quantity, limitPrice .limitPrice(300) boolean allOrNone def valueCalculation .allOrNone(true) Action action .valueAs{ qty, unitPrice -> qty * unitPrice - 100 } def buy(Integer quantity, String security) { this.quantity = quantity this.security = security println new Order() this.action = Action.Buy .buy(200, "GOOG") return this } .limitPrice(200) .allOrNone(true) © ASERT 2006-2010 def sell(Integer quantity, String security) { .valueAs{ qty, unitPrice -> this.quantity = quantity this.security = security qty * unitPrice - 500 } this.action = Action.Sell return this } def limitPrice(Integer limit) { this.limitPrice = limit; return this } Sell 150 shares of IBM at valuation of 44900 Buy 200 shares of GOOG at valuation of 39500 def allOrNone(boolean allOrNone) { this.allOrNone = allOrNone; return this } def valueAs(Closure valueCalculation) { this.valueCalculation = valueCalculation; return this } String toString() { "$action $quantity shares of $security at valuation of ${valueCalculation(quantity, limitPrice)}" } } DSLs 2010 - 148
  • …Stock Exchange Order DSL… From: customer@finance-broker.org To: Guillaume Subject: Project Request Date: Wednesday morning Dear Guillaume, © ASERT 2006-2010 That version was great but can you make the DSL a little more fluent. The brokers aren’t programmers after all! Thanks, Happy Customer DSLs 2010 - 149
  • …Stock Exchange Order DSL… class Order { def security def quantity def limitPrice def allOrNone def value def bs def buy(securityQuantity, closure) { bs = 'Bought' buySell(securityQuantity, closure) } def sell(securityQuantity, closure) { © ASERT 2006-2010 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] } ... DSLs 2010 - 150
  • …Stock Exchange Order DSL… ... newOrder.to.buy(100.shares.of(IBM)) { def getTo() { this } 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 allOrNone false class Quantity { valueAs { qty, unitPrice -> Security security qty * unitPrice - 500 } Integer quantity © ASERT 2006-2010 } } 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() DSLs 2010 - 151
  • …Stock Exchange Order DSL… From: customer@finance-broker.org To: Guillaume Subject: Project Request Date: Wednesday lunch time Dear Guillaume, © ASERT 2006-2010 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 DSLs 2010 - 152
  • …Stock Exchange Order DSL… class Order { newOrder.to.buy(100.shares.of(IBM)) { ... def allOrNone limitPrice 300 def valueCalculation allOrNone true def bs valueAs { qty * unitPrice - 500 } ... } private buySell(securityQuantity, newOrder.to.sell 200.shares.of(GOOG), { closure) { ... limitPrice 200 valueCalculation = c() allOrNone false // debug print resulting order valueAs { qty * unitPrice - 100 } println toString() } return this © ASERT 2006-2010 } ... 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 } String toString() { "$bs $quantity shares of $security.name and valuation of ${valueCalculation(quantity, limitPrice)}" } } DSLs 2010 - 153
  • …Stock Exchange Order DSL… From: customer@finance-broker.org To: Guillaume Subject: Project Request Date: Wednesday afternoon Dear Guillaume, © ASERT 2006-2010 Fantastic! This is getting better all the time! But can we get rid most of the ‘.’ and bracket symbols! Thanks, Happy Customer DSLs 2010 - 154
  • …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) { © ASERT 2006-2010 // 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 } String toString() { "$action $quantity shares of $security.name at limit price of $limitPrice" } } // ... http://groovyconsole.appspot.com/script/226001 by glaforge DSLs 2010 - 155
  • …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 thrown an exception and return null © ASERT 2006-2010 // 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 } DSLs 2010 - 156
  • …Stock Exchange Order DSL… // ---- Stock exchange orders 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 © ASERT 2006-2010 allOrNone true at the value of { qty * unitPrice } } http://groovyconsole.appspot.com/script/226001 by glaforge DSLs 2010 - 157
  • …Stock Exchange Order DSL From: customer@finance-broker.org To: Guillaume Subject: Project Request Date: Wednesday evening Dear Guillaume, © ASERT 2006-2010 Brilliant! Even our CEO could write orders! Thanks, Happy Customer DSLs 2010 - 158
  • THURSDAY © ASERT 2006-2010 http://t1.gstatic.com/images?q=tbn:_VBloEP8YC-6IM:http://msp248.photobucket.com/albums/gg171/ingrid2002/TextPL252028 DSLs 2010 - 159
  • 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. © ASERT 2006-2010 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 DSLs 2010 - 160
  • 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.” © ASERT 2006-2010 – 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 DSLs 2010 - 161
  • …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 © ASERT 2006-2010 – 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 – The man who smokes Blend has a neighbor who drinks water • And a question: – Who owns the fish? DSLs 2010 - 162
  • 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). © ASERT 2006-2010 % 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). ... DSLs 2010 - 163
  • Einstein‟s Riddle DSL … From: customer@acme.org To: Paul King Subject: Project Request Date: Early morning Dear Paul, © ASERT 2006-2010 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 DSLs 2010 - 164
  • Einstein‟s Riddle : Polyglot @GrabResolver('http://dev.inf.unideb.hu:8090/archiva/repository/internal') //@Grab('jlog:jlogic-debug:1.3.6') @Grab('org.prolog4j:prolog4j-api:0.2.0') // uncomment one of the next three lines //@Grab('org.prolog4j:prolog4j-jlog:0.2.0') @Grab('org.prolog4j:prolog4j-tuprolog:0.2.0') //@Grab('org.prolog4j:prolog4j-jtrolog:0.2.0') import org.prolog4j.* def p = ProverFactory.prover p.addTheory(new File('/GroovyExamples/tuProlog/src/einstein.pl').text) def sol = p.solve("solution(Persons).") © ASERT 2006-2010 //println sol.solution.get('Persons') // jlog to avoid converter println sol.get('Persons') // jtrolog/tuProlog DSLs 2010 - 165
  • Einstein‟s Riddle DSL … From: customer@acme.org To: Paul King Subject: Project Request Date: Early morning Dear Paul, © ASERT 2006-2010 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 DSLs 2010 - 166
  • 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' © ASERT 2006-2010 // 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). ''') DSLs 2010 - 167
  • …Einstein‟s Riddle : Polyglot w/ DSL… // define some helper methods (our interface to prolog) def addPairHint(Map m) { def from = m.from?.toString()?.toLowerCase() p.addTheory(""" hint$hintNum([(${from ?: '_'},${m.color ?: '_'},${m.drink ?: '_'},${m.smoke ?: '_'},${m.pet ?: '_'})|_]). hint$hintNum([_|T]) :- hint$hintNum(T). """) hintNum++ } def addPositionHint(Map m, int pos) { © ASERT 2006-2010 def from = m.from?.toString()?.toLowerCase() p.addTheory(""" hint$hintNum(Persons) :- person($pos, Persons, (${from ?: '_'},${m.color ?: '_'},${m.drink ?: '_'},${m.smoke ?: '_'},${m.pet ?: '_'})). """) hintNum++ } def addToLeftHint(Map left, Map right) { p.addTheory(""" hint$hintNum([(_,$left.color,_,_,_),(_,$right.color,_,_,_)|_]). hint$hintNum([_|T]) :- hint$hintNum(T). """) hintNum++ } ... DSLs 2010 - 168
  • …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]) }, rears: { p -> addPairHint(ctx + [pet:p]) }, owns:{ _the -> [first:{ house -> addPositionHint(ctx, 1) }] }, has:{ _a -> © ASERT 2006-2010 [pet: { a -> addPairHint(ctx + [pet:a]) }] + Color.values().collectEntries{ c -> [c.toString(), { _dummy -> addPairHint(ctx + [color:c]) } ] } }, lives: { _next -> [to: { _the -> Color.values().collectEntries{ c -> [c.toString(), { _dummy -> addNeighbourHint(ctx, [color:c]) } ] } }]} ] } ... DSLs 2010 - 169
  • …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 // alt’n8 end: keeps birds © ASERT 2006-2010 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 DSLs 2010 - 170
  • Einstein‟s Riddle DSL … From: customer@acme.org To: Paul King Subject: Project Request Date: Early morning Dear Paul, © ASERT 2006-2010 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 DSLs 2010 - 171
  • …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]) }, ... ] } © ASERT 2006-2010 ... 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]) ] DSLs 2010 - 172
  • …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: © ASERT 2006-2010 • 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 DSLs 2010 - 173
  • …Einstein‟s Riddle : Polyglot w/ DSL def the(Color c1) {[ house: { _is -> [on: { _the -> [left: { _side -> [of: { __the -> Color.values().collectEntries{ c2 -> [c2.toString(), { _dummy -> addToLeftHint([color:c1], [color:c2]) }]} }]}]}]} ]} © ASERT 2006-2010 class HousePlaceHolder { def c1, script def house(_is) { [on: { _the -> [left: { _side -> [of: { __the -> Color.values().collectEntries { c2 -> [c2.toString(), { _dummy -> script.addToLeftHint( [color: c1], [color: c2] )}]} }]}]}] } } def the(Color c1) { new HousePlaceHolder(c1:c1, script:this) } „house‟ is now understood DSLs 2010 - 174
  • Einstein‟s Riddle DSL … From: customer@acme.org To: Paul King Subject: Project Request Date: Early morning Dear Paul, © ASERT 2006-2010 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 DSLs 2010 - 175
  • 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) } © ASERT 2006-2010 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) } ... DSLs 2010 - 176
  • …Einstein‟s Riddle : Choco w/ DSL… // define DSL (simplistic non-refactored version) def neighbours(var1, val1, var2, val2) { and( ifOnlyIf(eq(var1[0], val1), eq(var2[1], val2)), implies(eq(var1[1], val1), or(eq(var2[0], val2), eq(var2[2], val2))), implies(eq(var1[2], val1), or(eq(var2[1], val2), eq(var2[3], val2))), implies(eq(var1[3], val1), or(eq(var2[2], val2), eq(var2[4], val2))), ifOnlyIf(eq(var1[4], val1), eq(var2[3], val2)) ) } iff = { e1, c1, e2, c2 -> and(*(0..<num).collect{ © ASERT 2006-2010 ifOnlyIf(eq(e1[it], c1), eq(e2[it], c2)) }) } ... // define the DSL in terms of DSL implementation def the(Nationality n) { def ctx = [nations, n] [ drinks:iff.curry(*ctx, drinks), smokes:iff.curry(*ctx, smokes), keeps:iff.curry(*ctx, pets), rears:iff.curry(*ctx, pets), owns:{ _the -> [first:{ house -> eq(nations[first], n)}] }, ... DSLs 2010 - 177
  • …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 © ASERT 2006-2010 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 ... DSLs 2010 - 178
  • …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) © ASERT 2006-2010 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() } • Output: 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 DSLs 2010 - 179
  • FRIDAY © ASERT 2006-2010 http://t3.gstatic.com/images?q=tbn:mgkj1lXpQ2-uWM:http://i298.photobucket.com/albums/mm269/bearyjuicylicious/comments DSLs 2010 - 180
  • Important business meetings all afternoon! © ASERT 2006-2010 http://www.flickr.com/photos/bobswanson/5093775403/sizes/l/in/pool-52240148330@N01/ DSLs 2010 - 181
  • Topics • Introduction • Groovy DSL Features • A Week In The Life Of DSL INC Summary • More Info © ASERT 2006-2010 DSLs 2010 - 182
  • Some Finishing Guidelines • Language and program evolve together … In the end your program will look as if the language had been designed for it … you end up with code which is clear, small, and efficient. – Paul Graham • Don‟t think just about Coding up your DSL – But also about its target audience, its evolution, required tool support and its testability DSLs 2010 - 183
  • Start small, with key concepts Beware over-engineering!
  • Grow your language progressively
  • Get your hands dirty Play with the end-users
  • Let your DSL fly, it‟s not yours, it‟s theirs!
  • Tight feedback loop Iterative process
  • Stay humble, You can‟t get it right the 1st time. Don‟t design alone at your desk Involve the end users from the start
  • Playing it safe in a sandbox
  • Topics • Introduction • Groovy DSL Features • A Week In The Life Of DSL INC • Summary More Info © ASERT 2006-2010 DSLs 2010 - 191
  • Further Info… • Compilers : Principles, Techniques, and Tools/ Edition 2, Alfred Aho, Ravi Sethi, Jeffrey Ullman, Monica Lam • Practical API Design : Confessions of a Java Framework Architect, Jaroslav Tulach © ASERT 2006-2010 • Language Implementation Patterns : Create Your Own Domain-Specific and General Programming Languages, Terence Parr DSLs 2010 - 192
  • …Further Info • A model-driven framework for domain specific languages, Martin Karlsch • Design Guidelines for Domain Specific Languages, Gabor Karsai et al • Program Comprehension for Domain- Specific Languages, Pereira et al © ASERT 2006-2010 • Diagrammatic Representations in Domain- Specific Languages, Konstantinos Tourlas • DSLs in Action, Debasish Ghosh • Domain-Specific Languages, Martin Fowler • DSLs in Boo : Domain Specific Languages in .NET, Ayende Rahien DSLs 2010 - 193
  • GinA 2ed “ReGinA” is coming! Contains a chapter on DSLs! DSLs 2010 - 194