b r a y d e n . o r g / Software

/ WebHome / LanguagePages / RubyLanguage / RubyRewriteMethods

This Web


WebHome  
Topic List  
Web Statistics 

All Webs


Books
Main
Random
Software
TWiki  

brayden.org


Home
Monthly Digest
Today's Links
Resumé
Reading List
Books RSS
Random RSS
Software RSS

Other


Dale's Blog

currently-reading
TextDrive

Rewriting Methods in Ruby

Taken directly from Programming Ruby.


The second example is a performance enhancement based on Tadayoshi Funaba's date module (described beginning on page 439). Say we have a class that represents some underlying quantity (in this case, a date). The class may have many attributes that present the same underlying date in different ways: as a Julian day number, as a string, as a [year, month, day] triple, and so on. Each value represents the same date and may involve a fairly complex calculation to derive. We therefore would like to calculate each attribute only once, when it is first accessed.

The manual way would be to add a test to each accessor:


class ExampleDate
  def initialize(dayNumber)
    @dayNumber = dayNumber
  end

  def asDayNumber
    @dayNumber
  end

  def asString
    unless @string
      # complex calculation
      @string = result
    end
    @string
  end

  def asYMD
    unless @ymd
      # another calculation
      @ymd = [ y, m, d ]
    end
    @ymd
  end
  # ...
end

This is a clunky technique---let's see if we can come up with something sexier.


class ExampleDate
  def asDayNumber
    @dayNumber
  end

  def asString
    # complex calculation
  end

  def asYMD
    # another calculation
    [ y, m, d ]
  end

  once :asString, :asYMD
end

We can use once as a directive by writing it as a class method of ExampleDate, but what should it look like internally? The trick is to have it rewrite the methods whose names it is passed. For each method, it creates an alias for the original code, then creates a new method with the same name. This new method does two things. First, it invokes the original method (using the alias) and stores the resulting value in an instance variable. Second, it redefines itself, so that on subsequent calls it simply returns the value of the instance variable directly. Here's Tadayoshi Funaba's code, slightly reformatted.


def ExampleDate.once(*ids)
  for id in ids
    module_eval <<-"end_eval"
      alias_method :__#{id.to_i}__, #{id.inspect}
      def #{id.id2name}(*args, &block)
        def self.#{id.id2name}(*args, &block)
          @__#{id.to_i}__
        end
        @__#{id.to_i}__ = __#{id.to_i}__(*args, &block)
      end
    end_eval
  end
end

This code uses module_eval to execute a block of code in the context of the calling module (or, in this case, the calling class). The original method is renamed _ nnn _, where the nnn part is the integer representation of the method name's symbol id. The code uses the same name for the caching instance variable. The bulk of the code is a method that dynamically redefines itself. Note that this redefinition uses the fact that methods may contain nested singleton method definitions, a clever trick.

However, we can take it further. Look in the date module, and you'll see method once written slightly differently.


class Date
  class << self
    def once(*ids)
      # ...
    end
  end
  # ...
end

The interesting thing here is the inner class definition, ``class << self''. This defines a class based on the object self, and self happens to be the class object for Date. The result? Every method within the inner class definition is automatically a class method of Date.

The once feature is generally applicable---it should work for any class. If you took once and made it a private instance method of class Module, it would be available for use in any Ruby class.

-- DaleBrayden - 19 Oct 2002

 
 
Current Rev: r1.3 - 25 Jun 2003 - 05:54 GMT - DaleBrayden, Revision History:Diffs | r1.3 | > | r1.2 | > | r1.1
© 2003-2011 by the contributing authors.