Ruby / Method aliasing and chaining

From WhyNotWiki
< Ruby (Redirected from Ruby / Method aliasing)
Jump to: navigation, search

See some examples at: http://svn.tylerrick.com/public/examples/ruby/method_aliasing/

Contents

Understanding method chains

This is especially a problem with Rails[Rails (category)], which sometimes has as many as 5 or so methods in an alias chain!

Method chains are made possible with alias_method_chain, which is part of (and is listed on this page) Facets.

Probably my best example to show how it works is this: http://svn.tylerrick.com/public/examples/ruby/method_aliasing/alias_method_chain-best.rb

Can you alias class methods?

Yes!

http://svn.tylerrick.com/public/examples/ruby/method_aliasing/using_alias_method_for_class_methods.rb

class Foo
  def a; 'a'; end
  alias_method :alias_for_a, :a

  def self.b; 'b'; end
  class <<self
    alias_method :alias_for_b, :b
  end
end

puts Foo.new.a
puts Foo.new.alias_for_a

puts Foo.b
puts Foo.alias_for_b

What if I want to do that from a module?

If you want to include your module...

http://www.redhillconsulting.com.au/blogs/simon/archives/000326.html

module ForeignKeyMigrations::Schema
  def self.included(base)
    base.extend(ClassMethods)
    base.class_eval do
      class << self
        alias_method :define_without_fk, :define unless method_defined?(:define_without_fk)
        alias_method :define, :define_with_fk
      end
    end
  end
  
  module ClassMethods
    def define_with_fk(info={}, &block)
      ...
      define_without_fk(info, &block)
    end
  end
end

http://svn.tylerrick.com/public/examples/ruby/class_methods_in_a_module_that_get_mixed_in-attempt_to_abstract_pattern_into_a_method_in_Module-4.rb

module M
  def self.included(base)
    base.class_eval do; class << self
      def class_method_with_M
        'From M'
      end
      alias_method_chain :class_method, :M
    end; end
  end
end

If you want to extend your module (which I ordinarily don't)...

http://snippets.dzone.com/posts/show/2133

module Foo
  def self.extended(object)
    class << object
      alias_method :to_s_without_foo, :to_s unless method_defined?(:to_s_without_foo)
      alias_method :to_s, :to_s_with_foo
    end 
  end

  def to_s_with_foo
    "#{to_s_without_foo} - got foo!"
  end

end

class X
end

X.extend(Foo)
puts X.to_s

Using alias_method_chain for class methods

http://svn.tylerrick.com/public/examples/ruby/method_aliasing//using_alias_method_chain_for_class_methods.rb


require 'rubygems'
require 'facets/core/module/alias_method_chain'


class Person
  def self.hello
    'Hello'
  end
end
module Fuddiness
  def self.included(base)
    base.extend ClassMethods
    base.class_eval do
      class << self
        alias_method_chain :hello, :fuddiness
      end
    end
  end

  module ClassMethods
    def hello_with_fuddiness
      hello_without_fuddiness.gsub('ll', 'w')
    end
  end
end
Person.send :include, Fuddiness
puts Person.hello




class String
  def self.foo(*args)
    'foo'
  end
  def self.foo_with_stuff(*args)
    'foo_with_stuff'
  end
  class << self
    alias_method_chain :foo, :stuff
  end
end
puts String.foo




# This method is just a tiny bit more concise.
require 'rubygems'
require 'qualitysmith_extensions/module/malias_method_chain'
class String
  def self.shoe(*args)
    'shoe'
  end
  def self.shoe_with_stuff(*args)
    'shoe_with_stuff'
  end
  malias_method_chain :shoe, :stuff
end
puts String.shoe
 


[Caveat (category)] method aliasing can lead to infinite loops

It happened to me. I've reproduced it in this test case if you want to try it or see how:

http://svn.tylerrick.com/public/examples/ruby/method_aliasing_can_lead_to_infinite_recursion-1.rb

But I'm not the only one it's happened to...

http://www.redhillconsulting.com.au/blogs/simon/archives/000326.html

(As a side note, the use of unless method_defined?(:to_s_without_quotes) is to work-around a bug in Ruby 1.8.4 that causes an infinite recursion when using alias_method. I never detected it under Mac OS X but apparently it affects Windows machines with monotonous regularity. D'oh!)

(It's happened my GNU/Linux workstation as well. So I don't think it's a Windows-only issue...)

Solution

A fail-safe method...

http://svn.tylerrick.com/public/examples/ruby/method_aliasing_can_lead_to_infinite_recursion-5.rb

# This is the simplest way to prevent the infinite recursion from happening...
# Just use qualitysmith_extensions/module/alias_method_chain, which will check if the '_without' method is already defined and, if so, will *not* attempt to

require 'rubygems'
require 'quality_extensions/module/alias_method_chain'

class Object
  def foo
    'foo'
  end
  def foo_with_stuff
    foo_without_stuff + '_with_stuff'
  end
  alias_method_chain :foo, :stuff
  alias_method_chain :foo, :stuff
  alias_method_chain :foo, :stuff
end
puts String.foo

Method chains in [Rails (category)]

This was my initial question...

So I see this reference to perform_action_without_benchmark here:

    def perform_action_with_benchmark
      ...
        runtime = [Benchmark::measure{ perform_action_without_benchmark }.real, 0.0001].max
      ...
    end

How do I know which method that's actually referring to?

It could be any of these...

> cgrep def.*perform_action
./vendor/rails/actionpack/lib/action_controller/benchmarking.rb:63:    def perform_action_with_benchmark
./vendor/rails/actionpack/lib/action_controller/filters.rb:618:        def perform_action_with_filters
./vendor/rails/actionpack/lib/action_controller/rescue.rb:81:          def perform_action_with_rescue #:nodoc:
./vendor/rails/actionpack/lib/action_controller/base.rb:1093:          def perform_action


We can assume that the perform_action defined here: ./vendor/rails/actionpack/lib/action_controller/base.rb will be invoked by the chain eventually. But we can't assume that perform_action_without_benchmark will end up being equivalent to the original perform_action (in base.rb) by the time perform_action_with_benchmark calls it!

This is because there are a bunch of members in the method chain. Any one of them could be next in line at the time we call __ ...

module ActionController #:nodoc:
  module Benchmarking #:nodoc:
    def self.included(base)
      base.class_eval do
        alias_method_chain :perform_action, :benchmark
      end
    end

> cgrep method_chain|grep perform_action
  rails/actionpack/lib/action_controller/filters.rb
  alias_method_chain :perform_action, :filters

  rails/actionpack/lib/action_controller/benchmarking.rb
  alias_method_chain :perform_action, :benchmark

  rails/actionpack/lib/action_controller/rescue.rb
  alias_method_chain :perform_action, :rescue

The order that those alias_method_chain statements are called does matter (as this example, http://svn.tylerrick.com/public/examples/ruby/method_aliasing/alias_method_chain-order_of_aliasing_matters.rb , shows), so let's take a look at the order in which they were included/called:


rails/actionpack/lib/action_controller.rb

ActionController::Base.class_eval do
  include ActionController::Filters
  include ActionController::Benchmarking
  include ActionController::Rescue
  ...
end

Following the pattern discovered in http://svn.tylerrick.com/public/examples/ruby/method_aliasing/alias_method_chain-best.rb , I can guess that this results in the following method aliases being created...

:perform_action                    => what was originally :perform_action_with_rescue    (and still is) (called *1st* since Rescue was the *last* to be included)
:perform_action_without_rescue     => what was originally :perform_action_with_benchmark (and still is)
:perform_action_without_benchmark  => what was originally :perform_action_with_filters   (and still is)
:perform_action_without_filters    => what was originally :perform_action                (but is no longer)

Let's see if we can test that theory. I put this in each of the _with_ methods, which caused a backtrace to be printed out for each of them.

      def perform_action
        pp caller(0)
        ...

This an excerpt from the backtrace in perform_action (base.rb) that listed all 4 methods (the other two methods only listed 3, 2, or 1 respectively):

["./script/../config/../vendor/rails/actionpack/lib/action_controller/base.rb:1094:in `perform_action_without_filters'",
 "./script/../config/../vendor/rails/actionpack/lib/action_controller/filters.rb:632:in `call_filter'",
 "./script/../config/../vendor/rails/actionpack/lib/action_controller/filters.rb:619:in `perform_action_without_benchmark'",
 "./script/../config/../vendor/rails/actionpack/lib/action_controller/benchmarking.rb:67:in `perform_action_without_rescue'",
 "/usr/lib/ruby/1.8/benchmark.rb:293:in `measure'",
 "./script/../config/../vendor/rails/actionpack/lib/action_controller/benchmarking.rb:67:in `perform_action_without_rescue'",
 "./script/../config/../vendor/rails/actionpack/lib/action_controller/rescue.rb:83:in `perform_action'",
 "./script/../config/../vendor/rails/actionpack/lib/action_controller/base.rb:430:in `send'",
 "./script/../config/../vendor/rails/actionpack/lib/action_controller/base.rb:430:in `process_without_filters'",
...

Cleaned up a little bit...

"rails/actionpack/lib/action_controller/base.rb:1094:       in `perform_action_without_filters'"     (called 4th)
...
"rails/actionpack/lib/action_controller/filters.rb:620:     in `perform_action_without_benchmark'",  (called 3rd)
...
"rails/actionpack/lib/action_controller/benchmarking.rb:69: in `perform_action_without_rescue'",     (called 2nd)
...
"rails/actionpack/lib/action_controller/rescue.rb:85:       in `perform_action'",                    (called 1st)
...

(Didn't see the perform_action that is defined in base.rb listed in the backtrace (it should have been called 4th). Why not?

Fortunately, even though the method names reported by the backtrace don't line up with reality (the names we gave the methods in the source code) (see below), we can just look at the respective line in the source code to see what it says. This is what I discovered when I did that:

base.rb:1094:       in `perform_action_without_filters'"  [is actually perform_action]
filters.rb:620:     in `perform_action_without_benchmark' [is actually perform_action_with_filters]
benchmarking.rb:69: in `perform_action_without_rescue'"   [is actually perform_action_with_benchmark]
rescue.rb:85:       in `perform_action'"                  [is actually perform_action_with_rescue]
 


Problem/caveat: the method names displayed in the backtrace may disagree with the names you originally gave it (in your source code)!

For example, even if the method was defined as perform_action_with_filters, the backtrace may report it as "perform_action_without_benchmark" (or some other "_without_/code>" method).

"rails/actionpack/lib/action_controller/filters.rb:620:in `perform_action_without_benchmark'"

Fortunately, we can always just look at the respective line it gives us (line 620 in filters.rb) in the source code to discover the real name of the method (in this case it was perform_action_with_filters).

It can be helpful to put each feature in its own file so that one can figure out the name of the method (even if it has been renamed) by looking at the filename). This is the convention Rails uses. So if the backtrace says filters.rb:620:in `perform_action_without_benchmark', we don't even have to open the file and go to line 620. We can just drop the _without_benchmark part from the method name and add _with_filters (getting the filters part straight from the filename, filters.rb).


alias_method_chain can accept a block

Currently (2007-04-05 10:15), only the ActiveSupport version accepts blocks, not the Facets version.

Here is an example of how you might use that...

./activesupport/lib/active_support/deprecation.rb:85

      # Declare that a method has been deprecated.
      def deprecate(*method_names)
        options = method_names.last.is_a?(Hash) ? method_names.pop : {}
        method_names = method_names + options.keys
        method_names.each do |method_name|
          alias_method_chain(method_name, :deprecation) do |target, punctuation|
            class_eval(<<-EOS, __FILE__, __LINE__)
              def #{target}_with_deprecation#{punctuation}(*args, &block)
                ::ActiveSupport::Deprecation.warn(self.class.deprecated_method_warning(:#{method_name}, #{options[method_name].inspect}), caller)
                #{target}_without_deprecation#{punctuation}(*args, &block)
              end
            EOS
          end
        end
      end
Ads
Personal tools