Recently, I rolled onto a mobile project with a web back end written in Ruby on Rails. This was my first experience with both Ruby and Rails.
The first feature work I did in the back end involved adding an administrative portal using Active Admin. I found myself negotiating the learning curve of not only a brand new language but also two very opinionated, convention-driven libraries.
The Problem
While building the new portal with Active Admin, I came across a baffling issue. Active Admin provides a method called ActiveAdmin.register
, used like so:
ActiveAdmin.register ActiveResource do
action_item :go_home do
if active_resource.go_home # What the.. ?
link_to "Home", home_path
end
end
end
This method automatically sets up several default pages relating to the ActiveResource
model. Any page relating to a particular instance of
ActiveResource
displays a “Go Home” button if that instance’s go_home
field is set to true
. The issue I had was in Line 3. Specifically: How is active_resource
bound to anything in the scope of that block?
Two Possibilities
There are two possible meanings active_resource
can have in Ruby: a local variable, or a method bound to self
. Ruby provides methods for determining available local variables and methods at runtime. Calling local_variables
returns an array of all local variables bound in the current scope. Calling methods
returns an array of all methods defined on self, self’s class, or any of self’s ancestors.
Neither of these methods produces an array containing active_resource
when called from with action_item
’s do block.
So what gives? If active_resource
isn’t bound to a local variable, and it’s not a method defined on self, how on earth is it possible that this name is magically bound inside of the action_item
method’s do block?
The Answer
The answer lies at the bitter end of the Ruby method resolution chain. First, Ruby checks if active_resource
is a local variable. We have already seen that this isn’t the case. Then, Ruby checks if active_resource
is a method defined somewhere in self’s ancestor chain. Again, this isn’t the case.
Total catastrophe, right? Wrong. Ruby has one more trick up its sleeve. Upon failing to find a method defined on self
, Ruby invokes method_missing
on self and passes in the intended method name. The root Object implements method_missing
, and simply throws the exception NameError: undefined local variable or method
.
Therefore, if the first method_missing
found in the ancestor chain is the one defined on the root Object, method resolution fails. Overriding method_missing
allows a developer to hijack this behavior.
Active Admin implements its magic by overriding method_missing
. Specifically, this method checks a hash object for the “method name” passed in and returns the corresponding value if found. Otherwise, it invokes super
, producing the normal error behavior. Active Admin essentially uses method_missing
to provide dynamic, runtime defined “local variables” in some contexts.
But Wait, There’s More!
So, having peeked behind the curtain, we know that overriding method_missing
can magically bind a “method name” to a value at runtime. But in this particular case, self
isn’t bound to the object that defines the ActiveAdmin.register
method. In other words, calling self outside the action_item
do block results in one object, while calling it inside the block results in another.
Using a method like instance_exec
or class_exec
achieves this behavior. Calling instance_exec
on an object binds that object to self
within the block passed to instance_exec
.
Putting It All Together
Here is a quick example of overriding method_missing
and using instance_exec
to produce some magical behavior:
class MagicClass
def initialize(lookup)
@lookup = lookup
end
def magic_method(&block)
instance_exec(&block)
end
def method_missing(name, *args, &block)
if @lookup.has_key?(name)
@lookup[name]
else
super
end
end
end
Using our magic class:
a = MagicClass.new(
{
three_clicks: "There's no place like home!",
magic: "Abracadabra!"
}
)
a.magic_method do magic end
# "Abracadabra!"
a.magic_method do three_clicks end
# "There's no place like home!"
a.magic_method do no_magic end
# NameError: undefined local variable or method `no_magic'
Conclusion
The instance_exec
and method_missing
methods in Ruby are two major building blocks in the implementation of many Ruby Domain-Specific Languages, or DSLs. DSLs are libraries that use runtime context to dynamically build methods following some naming convention, like Active Admin in this example.
Calling these generated methods with command call or variable call syntax allows developers to write code that looks remarkably like English. However, hiding the actual “binding” within method_missing
or some other abstraction prevents a developer from discovering available methods statically in the library’s source code. To be productive, a developer using a DSL needs to be familiar with the naming conventions of that particular DSL.
If you are interested in any more of the nitty-gritty details of Ruby’s implementation, this GitHub Wiki provides in-depth documentation of many facets of the Ruby language, including the concepts presented in this post.
Glad to see you enjoyed figuring this out. Ten years ago DSLs were trendy, today thankfully not so much. Interesting your first example is registering an ActiveAdmin action_item: it leads to issue reports like https://github.com/activeadmin/activeadmin/issues/5316 with confused and frustrated developers. Ruby DSLs can be the right tool for certain problems but beware the consequences.