Ruby DSLs aren’t

or, Ruby’s syntax is nice, now get over it. ;p

I already wrote a bit about this, as have others, but I’ve collected further examples and clarified my thinking on it a bit. The conclusion that I’ve come to is this: Domain-specific languages (DSLs) in Ruby, aren’t. That is, they aren’t DSLs.

Now, that may not be strictly true. I’ll give a (weak) counterexample to my own thesis toward the end. But I think RobertFischer was right when he said that the definition of DSL is bleeding over into meaning simply API. I propose that we need to be clear about what is and isn’t a DSL, so we all know what each other is talking about.

        <span id="more-859"></span>

        <p>Let me start with an example of what I consider to be a true internal domain-specific language: an imaginary <a href="http://en.wikipedia.org/wiki/Object-relational_mapping" title="Object-relational mapping - Wikipedia">object-relational mapping</a> &agrave; la ActiveRecord, but written in Lisp. To find all objects matching certain criteria, you might do the following:</p>
(find-all exhibitions (conditions (> run-time 2)
                                  (< run-time 5)))

What makes this a DSL instead of just a library? It would have to be implemented with a macro. exhibitions and run-time wouldn’t be variables; conditions wouldn’t be a procedure. Instead, find-all introduces a macro, and all of the arguments to find-all are processed as a list, not as variables and procedures to be applied. In effect, the find-all macro is a special-purpose parser, built to handle only this construct within the language. To do the same thing without using a macro would look like this:

(find-all 'exhibitions '(conditions (> run-time 2)
                                    (< run-time 5)))

(Note the extra apostrophes that mark symbols and literal lists.)

The first (macro-implemented) version clearly uses the syntax and expectations of the host language (Lisp), but creates a natural syntax to use for this particular function: querying the database with certain conditions. run-time isn’t a quantity in the language, but you can use the same syntax for comparison ((< run-time 5)) that you would if it were a Lisp quantity. This makes for more readable and writable code: the new syntax created follows your expectations.

Now let’s compare that with DataMapper’s “DSL” for querying:

exhibitions = Exhibition.all(:run_time.gt => 2, :run_time.lt => 5)

What’s actually going on here? We’re passing a hash to the Exhibition.all method. So far, so good. What’s in the hash? The values are integers, and the keys are the result of calling the #gt and #lt methods on the symbol :run_time. Wait… what? What do Symbol#gt and Symbol#lt mean?

I could argue that this is merely a bad DSL, but I won’t. I’m arguing that this actually isn’t a DSL at all. It’s a library method that takes some funny arguments and monkey-patches a core class to enable it. I don’t mean to denigrate it by saying it’s “just” a library method; I only want to call constructs by their real names. In order to make it a DSL, it would need to be called more like this:

exhibitions = Exhibition.all(run_time > 2, run_time < 5)

But Ruby can’t do that, because its syntax is fixed. The above would throw a NameError because run_time is undefined—and even if it were, it would result in a method call with two boolean values as arguments, clearly not the intent.

Which brings me to my point: You cannot write a domain-specific language in Ruby, because Ruby’s syntax is fixed. In order for an internal DSL to be created, by the definition I propose, it must be a discernibly separate language embedded within a host language. The DataMapper example above is just Ruby; it’s not a different syntax, nor is it different semantically. Nor, I argue, does it make it any easier to write queries. I’d rather just use ActiveRecord’s approach:

exhibitions = Exhibition.find(:all, :conditions => "run_time > 2 AND run_time < 5")

This doesn’t claim to be a DSL. It’s an SQL condition string passed to a method. It’s the API of a relatively well-designed and useful library. But that brings me to my counterexample: ActiveRecord’s macros for defining associations. I am willing to classify this as a DSL, for probably no rational reason:

class Exhibition < ActiveRecord::Base
  has_many :exhibitors
end

This is a class macro that functions much the same way as Ruby’s built-in attr_reader and attr_writer macros. Why do I consider these DSLs, flying in the face of my own point? Because they do something that Lisp might do with a macro: they write methods at runtime (specifically, at class-definition time). It’s not a syntactic manipulation, but it is metaprogramming. These macros also follow the expectations of writing in the host language.

(And actually, you can write methods at runtime in Lisp without using macros, so my argument for this being a DSL really is weak.)


About this entry