rkh / chainable

never use alias_method_chain, again

This URL has Read+Write access

name age message
file .gitignore Fri Mar 20 07:40:35 -0700 2009 ignore list added [rkh]
file LICENSE Fri Mar 20 09:23:54 -0700 2009 added license [rkh]
file README.rdoc Tue Mar 24 18:35:44 -0700 2009 install instructions [rkh]
file Rakefile Sun Mar 22 07:06:25 -0700 2009 spec fixes [rkh]
directory benchmark/ Mon Mar 23 06:26:09 -0700 2009 merge_method implemented [rkh]
file chainable.gemspec Tue Mar 31 05:37:10 -0700 2009 version dump [rkh]
directory lib/ Tue Mar 31 05:33:48 -0700 2009 fix typo [rkh]
directory spec/ Tue Mar 31 05:33:48 -0700 2009 fix typo [rkh]
README.rdoc

A word of warning:

This is heavy ruby abuse. It even got the Evil of the Day Award™ from zenspider.

Forks are welcome!

Thou shalt not use alias_method_chain!

What it does

Chaining Methods

Chainable is an alternative to alias_method_chain, that uses inheritance, rather than aliasing. It does the following when "chaining" a method:

  • copy the original method to a new model
  • include the model
  • overwrite the method

Thus you can use super and keep your method list clean, too! It even supports a (rather dangerous) auto chaining mode, so you do not have to explicitly chain a method, but chain a method whenever it would be overwritten instead.

Example:

  class Foo

    def foo
     10
    end

    # now chain to foo
    chain_method :foo do
      super + 3
    end

    # or turn on auto chaining
    auto_chain do

      def bar
        10
      end

      def bar
        super + 1
      end

      def bar
        super ** 2
      end

    end

    # or chain multiple methods at once
    chain_method :foo, :bar do
      super.to_s
    end

  end

  f = Foo.new
  puts f.foo # => 13
  puts f.bar # => 121

Of course you can do this with any class (or module):

  Array.class_eval do
    chain_method :each
    def each
      return super if block_given? or RUBY_VERSION >= "1.8.7"
      MyStuff::Enumerator.new self, :each
    end
  end

Note that there is a speed advantage when using chain_method without a block and doing a "def", since chain_method will use define_method if a block is given, which produces slower methods.

Merging Methods

But wait, there is more:

  class Foo
    def foo
      10
    end
    merge_method :foo do
      super * 3
    end
  end

  puts Ruby2Ruby.translate Foo, :foo

The output:

  def foo
    (10) * 3
  end

Before you yell at me about how insane I am, read on!

The library will only allow merging, if it thinks, it is possible:

  class Foo
    def foo
      x = 10
    end
    merge_method :foo do
      x = 20
      super
      puts x
    end
  end

Will give you:

   ArgumentError: cannot merge foo.

Same goes for this one:

   class Foo
     def foo x
       puts x
     end
     merge_method :foo do
       super
     end
   end

   # => ArgumentError: cannot merge foo.

But where is the fun in that one? You probably don’t want your ruby script throwing such errors at you.

Enter "try_merge":

  SomeEvilClassWithoutHooks.class_eval do
    chain_method *instance_methods(false), :try_merge => true do
      old_value = self.value.dup
      super.tap { observer.notify if old_value != value }
    end
    attr_accessor :observer
  end

  some_evil_instance.observer = MyObserver.new

When to use it?

As with alias_method_chain, you should use this as seldom as possible. Prefer clean inheritance over evil hacks. There actually is only one case one may use chainable (or alias_method_chain, for that matter): If there is a class you need to modify that is not part of your own code and the instances you deal with may already exists when you modify the class. In case you can modify the class before instance creation, just create another class inheriting from the first one and overwrite new to return instances of the latter.

Benchmark

chain_method tends do produce slightly faster methods than alias_method_chain:

  $ rake benchmark
                                           user     system      total        real
  no wrappers                          0.000000   0.000000   0.000000 (  0.004887)
  merge_method                         0.000000   0.000000   0.000000 (  0.004830)
  chain_method (def)                   1.040000   0.350000   1.390000 (  1.392329)
  chain_method (define_method)         1.150000   0.240000   1.390000 (  1.396007)
  alias_method_chain (def)             1.210000   0.260000   1.470000 (  1.472633)
  alias_method_chain (define_method)   3.470000   0.590000   4.060000 (  4.096245)

Installation

From RubyForge:

  gem install chainable

From GitHub:

  gem sources -a http://gems.github.com
  gem install rkh-chainable

Running test

The specs should work with rspec, mspec and bacon.