Object.memoize
# = 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