Implementing a DSL
Tim Wilde asked how I would build a DSL, given the example that I gave:
task "warn if website is not alive": every 3.Minutes() starting now when WebSite("http://example.org").IsAlive == false then: notify "admin@example.org", "server down!"
Now, I have a small confession to make, I didn't build the DSL for the post, I just thought it up and posted it. Tim has spiked a somewhat nicer implementation of the C# fluent interface, and it was about 250 lines of code, in 8 classes, 6 interfaces.
Face with that, I felt that I had no other choice than to go on and build a DSL implementation. If fact, I think that this is a good exercise of DSL building. We have started from the required syntax, and then we see how we can implement it.
I had to make two modifications to the DSL in order to make it really work, I marked them in red. One was that I wanted to implement the DSL in C#, and it has no way to write an indexed property, which I need in order to create extension properties in Boo, something that Boo completely supports. It was an interesting insight. The second was that the is operator is for reference types, not for values types, so I changed the IsAlive is false to IsAlive == false.
Most of my DSL use the Anonymous Base Class pattern. This means that you need to imagine a base class that wraps the code in this class. This is a really simple DSL, and I wanted to introduce as little complexity as possible, so we just use the basic capabilities of the language.
There are several interesting things that are going on in the DSL. For a start, we are using Boo's ability to drop parenthesizes if needed, which gives us a more "keyword" feeling. Another interesting approach is that when the last parameter of a method is a delegate, we can use just create a block and use that as a way to create the delegate.
So this code:
task "foo": print "bar"
Is the equivalent of this C# code:
task("foo", delegate { Console.WriteLine("bar"); });
This is how "task" and "then" keywords works.
public void task(string name, ActionDelegate taskDelegate) { this.taskName = name; taskDelegate(); }public void then(ActionDelegate actionDelegate) { this.action = actionDelegate; }
The "when" keyword is a lot more interesting. We want to do late evaluation of the expression that we pass to the when keyword, but the language doesn't allow us to do so. This is where the real power of Boo comes into place, we simply change the language.
[Meta] public static Expression when(Expression expression) { BlockExpression right = new BlockExpression(); right.Body.Add(new ReturnStatement(expression)); return new BinaryExpression( BinaryOperatorType.Assign, new ReferenceExpression("condition"), right ); }
This code (which looks much better when it is in Boo, by the way) means that whenever the compiler encounters the result of this statement:
when WebSite("http://example.org").IsAlive == false
Is actually this code:
condition = { return (WebSite("http://example.org").IsAlive == false) }
To my knowledge, this type of transformations is not available in other languages. In fact, while I am certainly not a Ruby expert, I do not believe that this is possible in Ruby. (Let us see if this brings the mob out of the woodworks, shall we?)
For the compiler, the code that we have above will be translated to:
public class validateWebSiteUp(Rhino.DSL.Tests.SchedulingDSL.BaseScheduler): public override def Prepare() as void: self.task('warn if website is not alive', def (): self.every(Rhino.DSL.Tests.SchedulingDSL.BaseScheduler.Minutes(3)) self.starting(self.now) self.condition = { return (WebSite("http://example.org").IsAlive == false) } self.then({ self.notify('admin@example.org', 'server down!') })
You can get the source for the DSL here:
A sample DSL is here:
Tests:
Comments
Thank you for taking the time to explain this Oren, I really appreciate it :)
I've got to admit "we simply change the language" makes my head spin a bit!
I'm going to dig through the Rhino-DSL source now :)
I will admit that "simply changing the lang" takes some time of getting used to :-)
Quite a few languages do this kind of task with no DSL fanfare at all. Here's a version in normal Rebol code:
site: http://www.example.com<br>
forever [<br>
prin [now "Checking site... "] <br>
either exists? site [print [site "ok"]] [print [site "ERROR!"]]<br>
wait 0:03<br>
]
Rebol is an example of dynamic languages in general (ruby, python, perl), but it is specifically designed to support building of DSLs.
Sorry; I wasn't sure if your comments permitted formatting.
Here's the Rebol code:
site: http://www.example.com
forever [
prin [now "Checking site... "]
either exists? site [print [site "ok"]] [print [site "ERROR!"]]
wait 0:03
]
I'm a little late, I just read the post today but I replicated your DSL using the Scheme dialect of Lisp: http://neverfriday.com/blog/?p=43
What do you think?
Comment preview