I've seen things you people wouldn't believe
Pete Forde of
Unspace emailed to say that the very first time he tried
andand, he discovered a bug:
nil
=> nil
nil.present?
=> false
nil.andand.present?
=> true
Reg:
Embarrassing.
Pete:
No sir. Not embarrassing, because no one’s ever going to find out they’re down here. Because you’re going to spot them, and you’re going to air them out.
#present? is defined in Edge Rails as:
class Object
def present?
!blank?
end
end
#blank? is also defined in Rails in Object, String, and NilClass. Hmmm. It didn’t take long to figure out what was happening. Internally, andand uses a
BlankSlate class to implement proxies for returning nil or for forwarding a method.
module AndAnd
class BlankSlate
instance_methods.each { |m| undef_method m unless m =~ /^__/ }
def initialize(me)
@me = me
end
end
class MockReturningMe < BlankSlate
def method_missing(*args)
@me
end
end
end
When you write nil.andand.present? What happens is this: nil.andand returns MockReturningMe.new(nil), and that object returns nil whenever you send it a method it doesn’t understand. Since it inherits from BlankSlate, we don’t expect it to understand #present?, so we expect it to return nil.
(Before you ask, nil.nil? returns true, but nil.andand.nil? returns nil. That’s intentional, because nil && nil.nil? returns nil, not true.)
So back to the bug. The BlankSlate wipes all of its instance methods out when it is first initialized. But what happens if the #present? method is added to Object
after andand initializes its BlankSlate? Ta da! The MockReturningMe class
does handle the method, so it never gets to use its method_missing magic.
And as it happens, a MockReturningMe object is not nil, therefore it is not blank, therefore it is present and #present? returns true.
If, that is, #present? is added to Object after AndAnd::BlankSlate is created. If #present? is added to Object
before AndAnd::BlankSlate, you get entirely different behaviour. WTF‽
I’m stating facts here, not rendering judgement: This is another example of multiple metaprogramming whatsis doohickeys all gleefully re-plumbing the same core classes and stepping on each other’s toes.
Even when they aren’t redefining the same methods.
Reg:
It seems you feel our work is not a benefit to the public.
Avdi:
Replicants are like any other machine. They’re either a benefit or a hazard. If they’re a benefit, it’s not my problem.
Update: Coderrr pointed out that the version of BlankSlate included in Rails (amongst other gems) fixes the problem by hooking the Object class. There are some subtleties in how this works or does not work, especially when several different pieces of code are all hooking the same events. After discussion with Nathan Weizenbaum, I decided that I certainly didn’t want andand being that invasive, but on the other hand if you have already decided you can live with a BlankSlate class…
So version 1.3.1 works as follows: If you already have a BlankSlate class defined, such as if you have installed Rails and used BlankSlate, or if you explicitly require BlankSlate before you require andand, andand will make use of the existing class and whatever mechanism that class uses to avoid this problem.
If you don’t have a BlankSlate class defined but you do have a BasicObject class (such as from Ruby Facets), andand will use that instead. And if you have neither a BlankSlate nor a BasicObject, andand will roll its own fairly uninvasive BlankSlate that wipes instance methods when it is instantiated (this is a performance hit compared to wiping them when they are created, however no hooks are required).
Pete is now happy. But this really encourages me to redouble my efforts on
rewrite. Opening core classes to add certain kinds of functionality is very cool. But I believe it is unsustainable.
Methods like #present?, #andand, and even #nil? really don’t seem to work as polymorphic methods on objects. They need to be deeper. For example, you can define your own class where #nil? return true:
class Nullo
def nil?
true
end
end
But guess what? This makes little sense in Ruby because your Nullo class is still truthy. (And no, you cannot write class Nullo < NilClass.)
I’ve been kvetching a little lately about Ruby not being turtles all the way down. I guess I’m doing that here as well: it is very hard to write things that extend or modify Ruby’s semantics consistently and safely, which i why I’m looking at rewriting.
With rewrite, for example, both #present? and #andand can be expressed as rewrite rules rather than as methods. (#blank? is a little bit more complicated, since you may want the luxury of writing a #blank? method for your own classes.) When you build semantic abstractions our of rewrite rules instead of methods in core classes, you benefit from restricted scope and you benefit from being able to work directly with Ruby’s existing semantics.