Too much cleverness

DataMapper is, in general, pretty cool, but like most Ruby developers lately, its developers seem to have seen all the “syntactic” fun you can have with the language and just taken it too far (“syntactic” in quotes because Ruby’s syntax is actually fixed). Here’s how DataMapper expects conditions to queries:

exhibitions = Exhibition.all(:run_time.gt => 2, :run_time.lt => 5)
# => SQL conditions: 'run_time > 1 AND run_time < 5'

And it does the same thing for ORDER BY clauses:

@zoos_by_tiger_count = Zoo.all(:order => [:tiger_count.desc])
# in SQL =>  select * from zoos ORDER BY tiger_count DESC

They actually defined methods on the Symbol class called lt, gt, like, not, in, etc. This is monkey patching almost at its worst. What use, in any Ruby code outside of an argument hash to DataMapper model, is being able to say :foobar.like(:a_duck) ?

One example of good monkey patching was (I think) introduced by Rails: the method Object#blank?, defined (basically) thus:

class Object
  def blank?
    nil? or (respond_to?(:empty?) and empty?)
  end
end

So it returns true on '' (an empty String), [] (an empty Array), and nil, and false on just about anything else (including objects that don’t have an empty? method). (The Rails implementation might also return true if you call false.empty?, as a special case. This might make sense; I haven’t thought the semantics through fully.) Object#blank? is a well-behaved monkey because it doesn’t modify existing, expected behavior, and it’s globally useful (throughout the Ruby language). It’s something that could be added to Ruby’s core without breaking things.

DataMapper’s Symbol extensions probably wouldn’t break other libraries, but the semantics are senseless. If a Symbol had a meaningful ‘less-than’ comparison to another symbol, it would just implement <=> and mix in Comparable. What the line

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

really means is that the column called ‘run_time’, which isn’t a Ruby quantity at all, should be between 2 and 5. A better way to say it would be:

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

or just

exhibitions = Exhibition.all("run_time BETWEEN 2 AND 5")

which is basically how ActiveRecord does it. Using a global monkey patch to Symbol to implement a syntactic translation of Ruby to SQL just muddies the semantics of Ruby without providing any extra clarity.

It’s a back door (through method definitions on symbols) into the kind of real syntax translations you can write as macros in Lisp (which don’t rely on the function invocation mechanism). You could define a macro conditions to be used as an argument to the finder method, which could then be invoked like this:

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

The difference is that the syntax you define only exists within the conditions macro.


About this entry