Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you extend a Ruby module with macro-like metaprogramming methods?

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?

like image 820
Ian Terrell Avatar asked Jun 10 '10 22:06

Ian Terrell


People also ask

When to use include VS extend Ruby?

In simple words, the difference between include and extend is that 'include' is for adding methods only to an instance of a class and 'extend' is for adding methods to the class but not to its instance.

What is macro style class method?

Class macros are class methods that are only used when a class is defined. They allow us to dry up shared code at across classes. In this post, we'll build a custom class macro that leverages class instance variables to define class-specific attributes.

What is extend in Ruby?

On the other hand, when we are using the extend keyword in Ruby, we are importing the module code but the methods are imported as class methods. If we try to access the methods that we imported with the instance of the class, the compiler will throw an error.

Does Ruby have macros?

Macros in Ruby are class methods that generate instance methods. Let us try to understand how macros work by implementing our own version of the has_many macro. has_many declaration is a call to the has_many class method. Upon invocation has_many dynamically generates methods for managing the association.


1 Answers

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.

like image 159
Jörg W Mittag Avatar answered Sep 28 '22 03:09

Jörg W Mittag