Class Methods Part IV - DSL’s

This last post in a series on Ruby class methods focuses on Domain Specific Languages.

There are many different types of DSL’s. The broadest classification, coined I believe by Martin Fowler, distinguishes between internal and external DSL’s. External DSL’s are languages with a language specific compiler or interpreter. If you are using YACC or ANTLR to define the grammar of your DSL, you’re writing an external DSL. Nathan Sobo presented an interesting tool at RubyConf07 for defining language parsers and interpreters. Treetop allows you to create an external DSL using a grammar definition DSL and then easily define the interpreter in Ruby. Here is a simple phone number parser…

The really interesting bit is the phone_number method defined on the phone_number rule. The ability to add methods to nodes allows for a lot of exciting interpreter possibilities.

The advantage of external DSL’s is unlimited expressiveness. As long as you can define a parser for your language, you can express your intent in whatever syntax maps most closely to your domain. An interesting example of an external DSL is ANSI SQL. I’m not referring to the general purpose procedural monstrosities implemented by various vendors, but rather to the small language specific to the domain of set manipulation. Another example is Make for the domain of software builds. All that expressiveness comes at a high cost. The big disadvantage of external DSL’s is that you have to define the grammar, build the parser, and build the interpreter/compiler. If the author doesn’t define conditional statements, the language doesn’t have conditional statements, etc…

Internal DSL’s are languages whose definitions mandate syntactically correct programs for a “hosting” general purpose language. The hosting language’s interpreter or compiler is then responsible for program execution. The advantages and disadvantages of internal DSL’s are an exact mirror to those of external DSL’s. On the up side, because an internal DSL is defined within the context of an existing general purpose language, the language designer picks up a lot of functionality for free. Someone has already written a parser and interpreter/compiler for the host language, and provided all the common features that one expects from a modern language. This leaves the language designer free to focus on a compelling domain syntax for his or her little language. There is of course a catch, the host language puts immutable restrictions on the expressiveness of an internal DSL. This can be more or less of a problem depending on the natural expressiveness of the host language. Ruby is a fairly expressive. Syntactic features like optional braces, optional parenthesis, do end blocks and inline conditionals allow for creative authors to declare a fairly intuitive DSL syntax without violating Ruby grammar.

Within the field of internal DSL’s Ruby allows for a variety of styles. Jamis Buck has written an excellent introduction to these styles. We’ll focus here on the DSL’s style Jamis calls Class Macros.

The most prominent example of a class macro based DSL in Ruby is ActiveRecord…

Some would argue that this sort of thing isn’t really a DSL at all. The definition of “DSL” is very slippery, and subject to a debate that is as useless as it is endless. I’ll use Jamis’s inclusion of it into the pantheon as my argumentum ad verecundiam.

This style of DSL is used to alter subclasses in an intuitive and domain expressive way. In some cases this becomes nothing more than an extremely fancy implementation of the Template Method pattern, where the template methods are defined based on instructions communicated via the DSL. In the extreme case, subclasses can be reduced to configuration files…

Note that HttpGettable is defined as a module not a base class. In general, in Ruby, if you think you need an Abstract Base Class, it pays to consider if the relationship is really an is a, or can be better expressed as a can. If it truly is an is a relationship, go ahead and create a Base class. There has been a lot of discussion as to how one can force a class to be abstract in Ruby. My rule of thumb is, unless someone might die if the class is instantiated, I’ll depend on a reasonable name (like AbstractGetter) to communicate the usage to other developers. If you are developing pacemakers, you might want to define initialize to raise an exception.

HttpGettable could also be implemented using the configuration to define what Kent Beck calls Constant Methods…

This implementation trades some of the metaprogramming nesting complexity in favor of a well known module include idiom. It provides for an aesthetically pleasing syntax, but at the cost of some complexity. One could also give up on DSL’s and use plain old constants…

So which option is best? That really depends on circumstances, aesthetic taste, and the teams ruby-fu as a whole. Skipping the whole DSL option in favor of a less expressive but much simpler implementation is certainly a valid option. Metaprogramming for metaprogramming’s sake is a bad idea that adds cost in complexity without compensation. The Gettable example is intentionally trivial to clearly demonstrate the various options. In the real world I probably would rely on constants for something so straightforward. When trying to express multiple complex predicates I lean towards the DSL style…

ActiveRecord is another, more robust, example of where this style is clearly warranted.

Thanks to Jay Fields, Jake Scruggs, and John Hume for their input on this topic. I’d like to recommend a new shortcut by my friend and colleague Philippe Hanrigou. Troubleshooting Ruby Processes: Leveraging System Tools when the Usual Ruby Tricks Stop Working is an excellent resource for learning the tools and techniques for resolving those incredibly frustrating “why does my app just sit there doing nothing?” defects. Hint: It probably isn’t a mongrel problem.

As always thanks for reading.

WordPress database error: [Table 'geyerindustriesblog.wp_comments' doesn't exist]
SELECT * FROM wp_comments WHERE comment_post_ID = '14' AND comment_approved = '1' ORDER BY comment_date

Comments are closed.