DZone Snippets is a public source code repository. Easily build up your personal collection of code snippets, categorize them with tags / keywords, and share them with the world

Snippets has posted 5883 posts at DZone. View Full User Profile

Object.memoize

04.01.2008
| 3927 views |
  • submit to reddit
        The following is inspired by the article "<a href="http://blog.grayproductions.net/articles/caching_and_memoization">Caching and Memoization</a>" by James Edward Gray II. 

# = Memoization for objects
# 
# This module will extend +Object+ with the +memoize+ method.  This method
# provides memoization for instance methods, which means return values will
# be cached and subsequent calls will return the cached value of the first
# call.
# 
# Caching is done based on instance, method and arguments to the method.  All
# data is kept in a single +Hash+ store which allows flushing all cached
# results at ones using the +flush_memos+ method.
# 
# == Example
#   class Person < Struct.new(:email)
#     def finger
#       `finger #{email}`
#     end
#     memoize :finger
#   end
#   
#   bob = Person.new('bob@test.net')
#   bob.finger                        # finger command executed
#   bob.finger                        # cached value returned
#   Memoizable.flush_memos
#   bob.finger                        # finger command executed
#
# == See also
# http://blog.grayproductions.net/articles/caching_and_memoization
#
# == Author
# R.W. van 't Veer, 2008-04-01, Amsterdam
module Memoizable
  # Store for cached values.
  CACHE = Hash.new{|h,k| h[k] = Hash.new{|h,k| h[k] = {}}} # 3 level hash; CACHE[:foo][:bar][:yelp]
  
  # Memoize the given method(s).
  def memoize(*names)
    names.each do |name|
      unmemoized = "__unmemoized_#{name}"
      
      class_eval %Q{
        alias   :#{unmemoized} :#{name}
        private :#{unmemoized}
        def #{name}(*args)
          cache = CACHE[self][#{name.inspect}]
          cache.has_key?(args) ? cache[args] : (cache[args] = send(:#{unmemoized}, *args))
        end
      }
    end
  end
  
  # Flush cached return values.
  def flush_memos
    CACHE.clear
  end
  module_function :flush_memos
end

class Object # :nodoc:
  extend Memoizable
end

if $0 == __FILE__
  require 'test/unit'
  
  class MemoizeTest < Test::Unit::TestCase # :nodoc:
    def setup
      @obj = TestObject.new
    end
    
    def teardown
      Memoizable.flush_memos
    end
    
    def test_memoize_value_should_stick_until_cache_flushed
      @obj.value = 'a'
      assert_equal 'a', @obj.value
      
      @obj.value = 'b'
      assert_equal 'a', @obj.value
      
      Memoizable.flush_memos
      assert_equal 'b', @obj.value
    end
    
    def test_flush_should_clear_all_cached_objects
      @obj.value = 'yelp'
      @obj.value
      
      assert_not_equal 0, Memoizable::CACHE.size
      Memoizable.flush_memos
      assert_equal 0, Memoizable::CACHE.size
    end
    
    def test_memoize_should_keep_separate_cache_per_instance
      other = TestObject.new
      @obj.value, other.value = 'a', 'b'
      
      assert_equal 'a', @obj.value
      assert_equal 'b', other.value
    end
    
    def test_memoize_should_keep_separate_cache_per_method
      @obj.value, @obj.other = 'a', 'b'
      
      assert_equal 'a', @obj.value
      assert_equal 'b', @obj.other
    end
    
    def test_memoize_should_include_arguments_in_cache_key
      @obj.with_arguments = 'a'
      assert_equal 'a', @obj.with_arguments(:this)
      
      @obj.with_arguments = 'b'
      assert_equal 'a', @obj.with_arguments(:this)
      assert_equal 'b', @obj.with_arguments(:that)
    end
    
    class TestObject # :nodoc:
      attr_accessor :value, :other
      memoize :value, :other
      
      attr_writer :with_arguments
      def with_arguments(*args); @with_arguments; end
      memoize :with_arguments
      
      attr_writer :question
      def question?; @question; end
      memoize :question?
      
      attr_writer :exclamation
      def exclamation!; @exclamation; end
      memoize :exclamation!
    end
  end
end