Composing DSLs in JavaScript

Further to my previous post I thought I’d share the approach I’ve been using to compose DSLs in JavaScript. If you want a more involved example, check out the currently in-development Forms module for Ojay.

For this example I’m going to be writing a simple permissions language that sets up rules about when certain methods may be called. It could be applied to a system for playing monopoly:

TheRulesAre(function() { with(this) {

    a(Player).mayNot('buyProperty').unless(function(player, property) {
        return property.owner === null &&
                player.funds >= property.price;
    });

    a(Property).mayNot('addHouse').when(it().isMortgaged());
}});

This illustrates a couple of principles when writing your DSL. First, I like to isolate DSL-based code inside a function. This allows cleaner separation from the rest of your program, so you can write helper functions inside the DSL function and not have them polute the global namespace. Second, you should write some test code first: it will make writing the DSL implementation much easier.

Let’s look at the pattern of the language we’re creating:

a(Class).mayNot(methodName).unless(condition)

Each rule takes a class and needs to modify one of its instance methods to check a condition before allowing the method to be called. The first step in implementing the DSL is to identify the root-level methods, which in this case are simply a(). This method should take a class and return an object with a mayNot method. Let’s also have methods an() (the same as a()) and the(), which will modify a specific object rather than a class.

var TheRulesAre = function(rules) {
    rules.apply(TheRulesAre.Root);
};

TheRulesAre.Root = {
    a: function(klass) {
        return new TheRulesAre.Restriction(klass.prototype);
    },
    an: function(klass) {
        return this.a(klass);
    },
    the: function(object) {
        return new TheRulesAre.Restriction(object);
    }
};

So now that we’ve set up the root methods, we need to implement mayNot(). This method will need to take a method name and return an object with unless() and when() methods. mayNot() will not modify the class itself, it will just need to return an object that will perform the modification. mayNot() is provided by the Restriction objects returned by the root-level methods (you’ll need JS.Class, including MethodChain to allow the it().isMortgaged() syntax):

TheRulesAre.Restriction = JS.Class({
    initialize: function(object) {
        this.object = object;
    },
    mayNot: function(methodName) {
        return new TheRulesAre.Condition(this.object, methodName);
    }
});

You see, we’re just passing the buck along, with some information about which object and which method to modify. The final piece of the puzzle is the Condition class, which implements the unless() and when() methods. Each of these should take a function (check) and modify the given method. The modification will consist of intercepting the method call and checking the receiver and the arguments using check. If check returns true, we allow the original method to run, otherwise we throw an exception.

TheRulesAre.Condition = JS.Class({
    initialize: function(object, methodName) {
        this.object = object;
        this.methodName = methodName;
    },

    unless: function(check) {
        // convert MethodChain objects
        if (check.toFunction) check = check.toFunction();

        var object = this.object, methodName = this.methodName;
        var original = object[methodName];

        object[methodName] = function() {
            var args = Array.from(arguments);
            var allowed = check.apply(null, [this].concat(args));
            if (allowed) return original.apply(this, args);
            else throw new Error(’method ‘ + methodName +
                    ‘() may not be called with these arguments’);
        };
    },

    when: function(check) {
        return this.unless(function() {
            return !check.apply(null, arguments);
        });
    }
});

And there you have it, a little mini-language for adding argument checking to method calls. The basic rule with these things is always, always write some test code first. It’s the only way to sort out your thinking about how the language should work and how it should be implemented.







5 Responses to “Composing DSLs in JavaScript”

The idea’s neat, but the syntax of this DSL is… difficult.

I’ll give you this much: This is the first time I’ve seen someone doing something like this.

Allain added these pithy words on Mar 28 08 at 8:07 pm

Without an equivalent to “method missing”, you are severely limited in what you can do with a Javascript DSL. However, this is a good example of about the best you can expect to get with today’s Javascript.

We’ll have to see what becomes of it, but Javascript 2 as currently drafted up might make it possible to improve the readability (and it has method missing support).

Jim LoVerde added these pithy words on Mar 29 08 at 5:39 am

Interesting… Do you see this ‘natural language’ approach as limited to the expression of rules which control access to object methods? How do you reconcile the externalizing of those rules with the traditional OO view that an object is an island… In other words, doesn’t this violate encapsulation? (I can see some cases where it does and it doesn’t.)

Have you seen: http://livepipe.net/projects/event_behavior/

It’s a very nice lib for ‘glue’ code… Looks like this:
with(Event.Behavior){
show(’state_field’).when(’country’).is(’United States’);
show(’province_field’).when(’country’).is(’Canada’);
}

Russell Allen added these pithy words on Mar 31 08 at 12:24 am

Jim, I would love a method_missing equivalent in JavaScript, but I don’t think that its absense really limits what you can do with the language. You can still do a lot of metaprogramming by generating functions dynamically and using reflection.

James added these pithy words on Apr 02 08 at 12:22 am

Russel, DSLs of the style demonstrated above do have a certain aptitude to writing descriptions — I like to think of them as executable specs. Typically, writing rules for a system is a good candidate for DSL writing. The concept of ‘rules’ does not need to be as narrow as controlling access to methods, though.

As regards breaking encapsulation, externalising the rules is often the whole point of APIs like the above. People frequently find that they end up scattering the rules for a system all over the different classes, making the system hard to modify and to add and remove rules easily. Separating the rules out of the classes lets you apply and revoke sets of rules at runtime, which is useful.

Sometimes, the best approach to designing a system is to get all the objects to implement the available actions, and put all the decision-making logic in a set of rules. I suppose the closest familiar OO concept for all this would be the mediator pattern, in which all the objects in a system communicate through a centralised resource. JavaScript’s extreme maleability means this pattern does not need a formalised mediator object but can simply involve directly modifying objects and classes.

James added these pithy words on Apr 02 08 at 12:25 am

Leave a Reply