Sunday, September 13, 2009

Vending Machine: A DSL in Groovy

In a previous post, I explained a modest example of the Vending Machine in Groovy. I've since taken the code and tried my hand at writing an internal DSL.

I'm a relative newbie to internal DSLs, but I've pushed my example up to GitHub (here). (This post is light on explaining the details, since the code is available.)

Observation #1

The best discovery is the magic of disappearing code. In this example, the main program simply evaporates and becomes this:


// load the DSL engine/rules
def dslEngine = new File("${args[0]}").text

// load the command input
def input = new File("${args[1]}").text.toLowerCase()

// dslEngine creates 'machine', which accepts the input:
def dslScript = " $dslEngine ; machine.accept { $input } "

// let Groovy do the rest!
new GroovyShell().evaluate(dslScript)
Amazing! There are 2 input arguments to the program. One is the DSL "engine" or context. It looks something like this:


class Machine {
def machineState = new MachineState()

def service(def coinList, def inventoryMap) {
machineState.availableChange = new MoneyState(coinList)
machineState.inventoryState = new InventoryState(inventoryMap)
}

def getN() { machineState.addInsertedMoney(MoneyState.NICKEL) }
def getD() { machineState.addInsertedMoney(MoneyState.DIME) }
def getQ() { machineState.addInsertedMoney(MoneyState.QUARTER) }

def getCoin_return() { machineState.insertedMoney = MoneyState.ZERO }

// snip
}

The other argument is the set of commands. For example:


SERVICE ([50, 50, 50, 50], [ [N:'A', P:'65', C:'10'] ])

VERIFY "[50, 50, 50, 50] [0, 0, 0, 0] [ [N:'A', P:'65', C:'10'] ]"

Note that the SERVICE command looks like a method call, with parentheses and a comma. That's because it is a method call. Similarly, VERIFY is as well, though no parentheses are necessary for the single string argument.

The other commands are simpler:

N ; D ; Q ; a$ ; COIN_RETURN
These are direct method/property calls as well (e.g. machine.getN()).

Observation #2

Articles on internal DSLs often talk about the contortions that one must go through to simplify the syntax for the end-user. Often, one uses techniques that would otherwise be considered poor style. (Venkat Subramaniam jokes that "designing the DSL" is "finding the right tricks").

I discovered that as well. In Groovy, it is relatively easy to have a decent DSL, but there is a never-ending desire to improve upon it. In the current version, I ran into a wall for using the dollar-sign as a token (see the compromise above: quoting that character in this post is giving Blogspot fits). Along with the parentheses on SERVICE, this pains me. I literally think about it while running on a treadmill.

Observation #3

In the first example, I adopted a file based approach for the input, over an interactive command-line. This has paid off in spades, because my suite of input files act as acceptance tests. Morphing the Java-esque example into a DSL was considerably easier with the existence of those files.

The upshot

With the right support and on the right scale, internal DSLs are terrific. For example, parts of (or, all of?) Grails and Gant are internal DSLs and I love it.

On a smaller scale, I'm not so sure. I'm still disturbed about the issue of Domain Specific Error messages. That is: can a domain expert (without development skills) really handle the power of an internal DSL (including the errors)?

Either way, internal DSLs are undeniably a fun exercise and a great way to learn more about a language.

2 comments:

  1. Oh, please, what a mess?!

    You will never get compiler checks or autocompletion this way.
    Users could never use such a language.

    Check out an MPS: www.jetbrains.com/mps
    Their documentation/tutuorials are far away from being perfecut, but the idea and implementation are brilliant.

    ReplyDelete
  2. @kosiakk What are you on about? It uses the groovy compiler doesn't it?

    ReplyDelete