The following is inspired by the article "
Caching and Memoization" by James Edward Gray II.
module Memoizable
CACHE = Hash.new{|h,k| h[k] = Hash.new{|h,k| h[k] = {}}}
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
def flush_memos
CACHE.clear
end
module_function :flush_memos
end
class Object
extend Memoizable
end
if $0 == __FILE__
require 'test/unit'
class MemoizeTest < Test::Unit::TestCase
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
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