The Pug Automatic

Dynamic super-overridable methods in Ruby

Written July 31, 2013. Tagged Ruby, Metaprogramming.

When you want to override methods defined by a library, super is more convenient than alias_method:

class Post < SomeORM
attributes :title, :body

def title
super || "No title"
end
end

But as these things are commonly implemented, you need to use alias_method (or Rails' alias_method_chain) instead, which is less convenient:

class Post
attributes :title, :body

alias_method :old_title, :title

def title
old_title || "No title"
end
end

I would encourage library writers to make their dynamically defined methods super-overridable whenever possible; this post explains how.

The problem

The common implementation uses define_method straight on your own class:

class SomeORM
def self.attributes(*names)
names.each do |name|
define_method(name) do
# Stuff
end
end
end
end

The library puts the method right in your class; right in Post. So if you define your own method by the same name in Post, you completely replace that old method, with no way to reach it. Thus you need alias_method.

The solution

But if the library could instead put the method further up the inheritance chain, we could define our own method and just call super.

So what's further up the inheritance chain? The SomeORM superclass of course, and its chain of superclasses. But we can't define methods there, or those methods would be available to every SomeORM subclass, not just Post.

What else is further up the inheritance chain? Modules.

Say we have this Ruby class:

class Post < SomeORM
include SomeModule
end

The (slightly simplified) inheritance chain would then be [Post, SomeModule, SomeORM, Object]. Included modules go between the class itself and its parent class, and super will look in those modules before it gets to the parent class.

So we simply create a module, define methods on that, and include it:

class SomeORM
def self.attributes(*names)
mod = Module.new
include mod

names.each do |name|
mod.module_eval do
define_method(name) do
# Stuff
end
end
end
end
end

That's all it takes.

Polishing the inheritance chain

What does the inheritance chain look like now? Something along the lines of [Post, #<Module:0x007fa0fea7fcf0>, SomeORM, Object].

We defined an anonymous module, which works fine but could be confusing in a later debugging session.

Also, the current implementation will create a separate module for each attributes declaration. If you define ten attributes each on their own line, you'll add ten modules to the inheritance chain.

While this works, it makes for a messier inheritance chain.

We can solve both these issues in one fell stroke:

class SomeORM
MODULE_NAME = :DynamicAttributes

def self.attributes(*names)
if const_defined?(MODULE_NAME, _search_ancestors = false)
mod = const_get(MODULE_NAME)
else
mod = const_set(MODULE_NAME, Module.new)
include mod
end

names.each do |name|
mod.module_eval do
define_method(name) do
# Stuff
end
end
end
end
end

(I provided a somewhat different solution earlier; this improved solution is due to Avdi Grimm.)

The added code does a few things. It names the constant Post::DynamicAttributes, so now the inheritance chain will be something like [Post, Post::DynamicAttributes, SomeORM, Object].

It checks if Post::DynamicAttributes already exists. If it does, it's reused. This means we only add a single module to the inheritance chain even if Post declares ten attributes each on their own line.

The second argument in const_defined?(MODULE_NAME, _search_ancestors = false) is important, by the way. Say we introduce a VideoPost that inherits from Post:

class VideoPost < Post
attribute :timestamp
end

Since we passed false as the second argument of const_defined?, we will not reuse Post::DynamicAttributes from the superclass, but will instead get a new VideoPost::DynamicAttributes module. This ensures attributes inherit correctly.

You could of course write just const_defined?(MODULE_NAME, false) without the _search_ancestors local variable. I feel it makes a cryptic argument more obvious, but it does trigger an "unused variable" warning in Ruby <2.0 with the -w flag.

Examples in the wild

I've implemented this in Traco and Minimapper.

Implementing it in Traco last December was inspired by points brought up in Ruby Rogues episode 80, in turn based on the talk "The Polite Programmer's Guide to Ruby Etiquette" by Jim Weirich et al. (which I haven't watched).

I'd love to hear about other libraries that get this right, alternative solutions, or problems with this approach.