tags:

views:

228

answers:

3

I'm looking to create a new DSL using groovy, but I'm having trouble figuring out the best way to have groovy read the dsl. I want users to be able to create a dsl and actually run the dsl, without having to talk to the application code.

game.groovy (as the dsl):

import com.foo.groovygame.Game

go north 10
search
find ana
turn right

I want users to be able to say:

groovy game.groovy

and it will run the game. I realize that I could do something like:

groovy Game game.groovy

but I would prefer to be able to run the dsl directly.

I noticed on this blog the author is using

Ronald.init(this)

I could do something like this, and have the init method handle the game from the dsl, but I was wondering if this is the best approach? It seems a little sloppy. I really like the ruby way of handling this. You can simply create a dsl and specify a require 'foo' in the dsl.

+1  A: 

I spent some time today experimenting with different ways of accomplishing what you describe, and I decided that the suggestion on the "Perfect Storm" blog you linked is probably the most straightforward and least convoluted.

My first thought was to create a static initializer in the Game class that would automatically take care of initializing the game, but unfortunately, even though the Game class is loaded just from the 'import' statement (as evidenced by running with -verbose:class set in JAVA_OPTS), the static initializers aren't executed until the Game class is referred to in some way. This requires a new Game() line in your game.groovy DSL.

Even assuming that you're okay with that, the only way to handle the functions and properties in your game DSL is to add support to the game class in some way. In a static initializer, you won't have access to the game class directly, but you'll have access to its superclass: groovy.lang.Script. You can add methods like go() and search() to Script.metaClass, but you're then adding them for all instances of Script, which is almost certainly not what you want.

This leads to needing to call some method of Game with the game DSL as an argument, a la Game.init (this). One thing that you can do to make it a little visually cleaner is to statically import Game's init method:

import static com.foo.groovygame.Game.init

init (this);
go north, 10
...

One last note: you'll still need to use Groovy syntax in your DSL, which means commas between method arguments, parentheses for method calls without arguments, etc.:

go north, 10
search ()
find ana
turn right

I'm very interested if others are able to come up with cleaner solutions, or even just some way to cause static initializers to be executed without having to refer to the enclosing class.

RTBarnard
+1  A: 

I really am not crazy about using a compilers syntax to implement a DSL--Sorry, I know some people like it and I readily admit that it's a cute trick, but it's so easy to write your own parser, why not do it? Then you don't have random commas and underscores scattered throughout your text.

Here's an easy trick I've used to implement a simple syntax like the one you describe:

First of all, look at your commands--note that most are in the format of "verb noun params"

This maps really well to methodName, objectName, params

So a nice procedure would be:

split sentence into string array s

for a line with a single word (if s.length == 1):
    instantiate an object with that name
    call a default method on that object
    done
for a line with more than one word
    instantiate the object s[1]
    call method s[0] with s[2...] as parameters
    done

This simple 5-10 or so line parser will solve many of your DSL type problems. Beyond that you can add features pretty easily:

If the parameters (2...) are in the form "name=value", scan for a parameter named "name" and pass "value" for that specific parameter. This probably wouldn't work in this specific case, but can be good for other uses.

If your single-word commands require parameters, then attempt to instantiate s[0] as a class even if there are multiple words. If this fails, revert to the multi-word algorithm above.

I had one case where I needed to keep objects around after they were instantiated. I used the syntax:

find person:ana 

(the syntax could be fixed back to your original syntax by keeping a table mapping ana to person and checking this table along with trying to instantiate objects)

and from then on, ana was an instance of the person class (in other words, after instantiating "person" and calling the method "find" on it, I stored the person object in a hash under the name "ana", the next time they used a command like:

talk ana

It would search the hash first, grab the object stored in there and call "talk" on that existing object (at which point it might check to see if ana had a "found" flag set, if not it might return a different message). In this way, you could have multiple friends, each with all their own state information.

This system has some limitations, but is still much more flexible than a Ruby style DSL and really isn't at all difficult to implement.

Bill K
A: 

RTBarnard's answer seems to be the best. What would be the best way to do some processing after the script was executed?

Ronald.init this

go right
back ten
...etc...

After all the dsl has been processed, then Ronald should do some additional work, but without the user having to say something such as Ronald.run().

holden