Pay No Attention: Behind the Magic of Ruby DSLs

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.

 
Conversation
  • Piers C says:

    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.

  • Comments are closed.