Ruby – How to extend a Ruby module with macro-like metaprogramming methods

metaprogrammingmoduleruby

Consider the following extension (the pattern popularized by several Rails plugins over the years):

module Extension
  def self.included(recipient)
    recipient.extend ClassMethods
    recipient.send :include, InstanceMethods
  end

  module ClassMethods
    def macro_method
      puts "Called macro_method within #{self.name}"
    end
  end

  module InstanceMethods
    def instance_method
      puts "Called instance_method within #{self.object_id}"
    end
  end
end

If you wished to expose this to every class, you can do the following:

Object.send :include, Extension

Now you can define any class and use the macro method:

class FooClass
  macro_method
end
#=> Called macro_method within FooClass

And instances can use the instance methods:

FooClass.new.instance_method
#=> Called instance_method within 2148182320

But even though Module.is_a?(Object), you cannot use the macro method in a module.

module FooModule
  macro_method
end
#=> undefined local variable or method `macro_method' for FooModule:Module (NameError)

This is true even if you explicitly include the original Extension into Module with Module.send(:include, Extension).

For individual modules you can include extensions by hand and get the same effect:

module FooModule
  include Extension
  macro_method
end
#=> Called macro_method within FooModule

But how do you add macro-like methods to all Ruby modules?

Best Answer

Consider the following extension (the pattern popularized by several Rails plugins over the years)

This is not a pattern, and it was not "popularized". It is an anti-pattern that was cargo-culted by 1337 PHP h4X0rZ who don't know Ruby. Thankfully, many (all?) instances of this anti-pattern have been eliminated from Rails 3, thanks to the hard word of Yehuda Katz, Carl Lerche and the others. Yehuda even uses pretty much the exact same code you posted as an anti-example both in his recent talks about cleaning up the Rails codebase, and he wrote an entire blog post just about this one anti-pattern.

If you wished to expose this to every class, you can do the following:

Object.send :include, Extension

If you want to add it to Object anyway, then why not just do that:

class Object
  def instance_method
    puts "Called instance_method within #{inspect}"
  end
end

But how do you add macro-like methods to all Ruby modules?

Simple: by adding them to Module:

class Module
  def macro_method
    puts "Called macro_method within #{inspect}"
  end
end

It all just works:

class FooClass
  macro_method
end
#=> Called macro_method within FooClass

FooClass.new.instance_method
#=> Called instance_method within #<FooClass:0x192abe0>

module FooModule
  macro_method
end
#=> Called macro_method within FooModule

It's just 10 lines of code vs. your 16, and exactly 0 of those 10 lines are metaprogramming or hooks or anything even remotely complicated.

The only difference between your code and mine is that in your code, the mixins show up in the inheritance hierarchy, so it is a tad easier to debug, because you actually see that something was added to Object. But that is easily fixed:

module ObjectExtensions
  def instance_method
    puts "Called instance_method within #{inspect}"
  end
end

class Object
  include ObjectExtensions
end

module ModuleExtensions
  def macro_method
    puts "Called macro_method within #{inspect}"
  end
end

class Module
  include ModuleExtensions
end

Now I'm tied with your code at 16 lines but I would argue that mine is simpler than yours, especially considering that yours doesn't work and neither you nor I nor almost 190000 StackOverflow users can figure out why.

Related Topic