The latest change to edge Merb doesn’t so much change the end-user functionality as it fixes a long-standing issue with how inheritable accessors work. If you’re mostly interested in how Merb work for an end-user, and not so much with Merb’s internals, it’s safe to skip this post.
Let’s start easy, with regular accessors:
class Foo
attr_accessor :bar
end
@foo = Foo.new
@foo.bar = 7
@foo.bar #=> 7
Regular attr_accessors are part of Ruby proper, and allow you to easily create accessor methods for instance variables in a class. The above code roughly converts to:
class Foo
def bar
@bar
end
def bar=(obj)
@bar = obj
end
end
This works great for simple instance variables. However, Ruby also has class variables, and Rails (as well as Merb) provides a simple way to bind methods to class variables:
class Foo
cattr_accessor :bar
end
Foo.bar = 7
Foo.bar #=> 7
That works great too, subject to the normal rules of class variables. Specifically, class variables are shared by a class and all of its subclasses, so they cannot be used to implement behavior specific to different subclasses (for instance, specific Controllers). Here’s a quick demonstration:
class Merb::Controller
cattr_accessor :blinking
end
class First < Merb::Controller
self.blinking = true
end
class Second < Merb::Controller
self.binking = false
end
First.blinking #=> false
As you can see, cattr_accessors cannot be used for controller-specific behavior. The next thing to try would be class instance variables:
class Merb::Controller
class << self
attr_accessor :blinking
end
end
class First < Merb::Controller
self.blinking = true
end
class Second < Merb::Controller
self.binking = false
end
First.blinking #=> true
Second.blinking #=> false
At first glance, this seems like the ticket. Sadly, this is not the way these sorts of accessors are usually used. What Merb (and Rails) wants to do is be able to set a preference on Merb::Controller and have it automatically inherit onto the child controllers unless specifically overridden. Like:
class Merb::Controller
attr_inheritable_accessor :blinking
self.blinking = false
end
class First < Merb::Controller
self.blinking = true
end
class Second < Merb::Controller
end
First.blinking #=> true
Second.blinking #=> false
In particular, we use this for things like layout location, which we want to allow to be declared in the Application controller, and have automatically percolate down to other controllers. This is also used in plugins quite a bit, where Merb::Controller will get a default inheritable attribute, which can be overridden at a lower level.
The Rub
Rails comes with an inheritable accessor, which, until now, Merb used. Unfortunately, it has the following behavior:
class Merb::Controller; end
class First < Merb::Controller; end
class Merb::Controller
class_inheritable_accessor :blinking
self.blinking = true
end
First.blinking #=> nil
To make a long story short, the reason for this is that the accessor gets inherited at the same time as the class gets inherited, which is before the accessor was defined (in this case). In practice, this leads to extremely brittle load order, as it is fairly common practice for plugins to add inheritable_accessors, and if the plugin is loaded in after the application’s controllers, very difficult-to-debug behavior can result.
A survey of #merb revealed that this enigmatic issue had struck quite frequently, so it was time to resolve the issue.
The New Behavior
In attacking the issue, we decided that the new inheritable accessor had to support the following behaviors:
- Resolve the load-order issue. Child classes should continue to look up the inheritance chain until a superclass has stored an accessor.
- Support ||= to store a child accessor. if Parent.foo nil, Child.foo ||= "Hello" will result in Parent.foo remaining nil, but Child.foo “Hello”
- Support << to modify a parent object. If Parent.foo [1], Child.foo << 2 will result in Parent.foo remaining [1], but child.foo [1,2]
- Support modifying child objects in place. If Parent.foo [1,2,3], Child.foo.reverse! will result in Parent.foo remaining [1,2,3], but Child.foo [3,2,1].
- Support similar semantics for a Hash. If Parent.foo {:x => "y"}, Child.foo.merge!(:z => "a"} will result in Parent.foo remaining the same, but Child.foo {:x => “y”, :z => “a”}
We were able to achieve all of the above, with one caveat:
Parent.foo = "Hello"
Child.foo
Parent.foo = "Goodbye"
Child.foo #=> "Hello"
This is because the way the above behavior is implemented is by storing the parent value on the first READ, which turns out to be much more versatile and correct that storing the parent on inherit. As it turns out, it’s almost always the case that the first read of an inheritable attribute happens at runtime, while the writes happen at boot-time, which makes this a perfectly reasonable tradeoff.
Of course, as I said above, this is a fairly esoteric change that doesn’t affect the average user of the framework, but it does dramatically reduce certain strange behavior from plugins.