In a recent discussion on the Rails-core mailing list, Joe Smith raised an issue that bothered me while I was working on my fork of Arel: predicate negation (not) has odd left-to-right readability. While only a minor annoyance, it did get me to spend some time tonight investigating a possible solution. This was when I got acquainted with Ruby 1.9′s BasicObject class, and more specifically,
So, in Ruby 1.8, you could always overload most “operators.” I quote the word operators, because in Ruby, the reason you can overload them is because they’re actually implemented as methods on objects, not built-ins. For example:
ruby-1.8.7-p249 > 1.class => Fixnum ruby-1.8.7-p249 > 1.methods.grep /\+|-|\*|\/|<</ => ["<<", "*", "+", "-", "/", "+@", "-@", "**"] ruby-1.8.7-p249 > 1.+(1) => 2 class Fixnum def **(other) puts "Sorry, but the exponent operator is out for repairs. Check back Tuesday." self end end ruby-1.8.7-p249 > 1 ** 2 Sorry, but the exponent operator is out for repairs. Check back Tuesday. => 1
You get the idea. Object-oriented programming 101. So, under the hood, Ruby is just a little less picky about that whole “period before the method name” rule on some methods to make those methods feel like operators, but they’re methods all the same. Except for a few special cases. Like the logical not operator, for instance.
ruby-1.8.7-p249 > fixnum.send('!') NoMethodError: undefined method `!' for 1:Fixnum
Sure enough, scan through the documentation for Ruby 1.8.x, and you will see a ton of “operator” methods:
**, and so on. But nowhere a
!. What a shame.
BasicObject to the rescue!
Now, let’s have a look at the situation in Ruby 1.9.
ruby-head > 1.send('!') => false ruby-head > o = Object.new => #<Object:0x0000010101a7a0> ruby-head > o.! => false
So, not only does Fixnum respond to a
! method call, but so does Object. And sure enough, if we take a look at Object’s Ruby 1.9 documentation, we see that something is different.
BasicObject is the parent class of all classes in Ruby. It‘s an explicit blank class. Object, the root of Ruby‘s class hierarchy is a direct subclass of BasicObject.
Well, this is new. Taking a look at BasicObject’s documentation, there it is, in all its splendor:
BasicObject#!! The second exclamation point is mine. Because this is really cool! Right?
Pardon my nerdery for a moment
In Ruby, we normally expect
!something to return true or false. If
true, and in every other case,
false. In fact,
!!object (not not object) is a Ruby idiom for “give me the true/false value of an object, not the object itself.” This works because
not is an operator that returns the complement of the boolean value of its operand. So our “double negative” gets us the value we started with (albeit cast to boolean now).
This is useful. But, might there be another useful meaning for “not”? Like, oh, say, one for sets of data that don’t consist of just two values, true and false? You know, like the sets of data in a database which are represented by an SQL query? Well, that would sure be nifty, wouldn’t it?
Back to the task at hand
So, let’s make use of this Ruby 1.9 feature to improve my Arel fork’s “not” syntax.
def complement Not.new(self) end alias not complement if respond_to?('!') def empty? false end define_method('!') do self.complement end end
As Peter Parker’s Uncle Ben taught us, “with great power comes great responsibility,” and this is no exception. If you’re overriding something like !, you may be surprised at the ways in which it causes breakage. For instance, we have to add an
empty? method to prevent the Rails core extension
Object#blank? from behaving as though every predicate is blank.
This is because Object#blank? is implemented like so:
respond_to?(:empty?) ? empty? : !self
Needless to say, since !self on a Predicate will always return another Predicate, which evaluates to true because it’s neither nil
nor false`, it caused Arel’s specs to go haywire until I added an empty? method. This is just another reason unit tests are important.
Anyway, with this change in place, we can now do the following on Ruby 1.9:
ruby-head > articles = Article.scoped.table => #<Arel::Table ...> ruby-head > Article.where(!articles[:title].eq('Hey!')).to_sql => "SELECT \"articles\".* FROM \"articles\" WHERE (\"articles\".\"title\" != 'Hey!')"
You’ll notice that this is slightly different than what you would get in the previous master branch, which would have used
NOT(articles.title = 'Hey!') instead. This is because to further flesh out the idea of complements, each Predicate overrides the default complement method to return a Predicate that is its true complement (Equality’s complement is Inequality, LessThan’s is GreaterThanOrEqualTo, etc). You can see the full commit here.
The end result is that double-negation of a predicate (or quadruple, for that matter) won’t continually add NOTs to the resulting SQL, and instead intelligently generates complement predicates as many levels deep as you go.
ruby-head > Article.where(!!!!!!articles[:title].eq('Hey!')).to_sql => "SELECT \"articles\".* FROM \"articles\" WHERE (\"articles\".\"title\" = 'Hey!')"
Anyway, this was a fun little exercise for me, and I hope it was at least mildly interesting to you, too.comments powered by Disqus