From 24403498ba8582d29f55aab16ffd5920dec1c669 Mon Sep 17 00:00:00 2001 From: Nicholas Seckar Date: Mon, 27 Mar 2006 03:59:35 +0000 Subject: Add CachingTools::HashCaching to simplify the creation of nested, autofilling hashes. git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4059 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- activesupport/CHANGELOG | 2 + activesupport/lib/active_support/caching_tools.rb | 62 +++++++++++++++++ activesupport/test/caching_tools_test.rb | 81 +++++++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 activesupport/lib/active_support/caching_tools.rb create mode 100644 activesupport/test/caching_tools_test.rb diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index dbe6aa91c2..87a8428d0f 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,5 +1,7 @@ *SVN* +* Add CachingTools::HashCaching to simplify the creation of nested, autofilling hashes. [Nicholas Seckar] + * Remove a hack intended to avoid unloading the same class twice, but which would not work anyways. [Nicholas Seckar] * Update Object.subclasses_of to locate nested classes. This affects Object.remove_subclasses_of in that nested classes will now be unloaded. [Nicholas Seckar] diff --git a/activesupport/lib/active_support/caching_tools.rb b/activesupport/lib/active_support/caching_tools.rb new file mode 100644 index 0000000000..c889c148b4 --- /dev/null +++ b/activesupport/lib/active_support/caching_tools.rb @@ -0,0 +1,62 @@ +module ActiveSupport + module CachingTools #:nodoc: + + # Provide shortcuts to simply the creation of nested default hashes. This + # pattern is useful, common practice, and unsightly when done manually. + module HashCaching + # Dynamically create a nested hash structure used to cache calls to +method_name+ + # The cache method is named +#{method_name}_cache+ unless :as => :alternate_name + # is given. + # + # The hash structure is created using nested Hash.new. For example: + # + # def slow_method(a, b) a ** b end + # + # can be cached using hash_cache :slow_method, which will define the method + # slow_method_cache. We can then find the result of a ** b using: + # + # slow_method_cache[a][b] + # + # The hash structure returned by slow_method_cache would look like this: + # + # Hash.new do |as, a| + # as[a] = Hash.new do |bs, b| + # bs[b] = slow_method(a, b) + # end + # end + # + # The generated code is actually compressed onto a single line to maintain + # sensible backtrace signatures. + # + def hash_cache(method_name, options = {}) + selector = options[:as] || "#{method_name}_cache" + method = self.instance_method(method_name) + + args = [] + code = "def #{selector}(); @#{selector} ||= " + + (1..method.arity).each do |n| + args << "v#{n}" + code << "Hash.new {|h#{n}, v#{n}| h#{n}[v#{n}] = " + end + + # Add the method call with arguments, followed by closing braces and end. + code << "#{method_name}(#{args * ', '}) #{'}' * method.arity} end" + + # Extract the line number information from the caller. Exceptions arising + # in the generated code should point to the +hash_cache :...+ line. + if caller[0] && /^(.*):(\d+)$/ =~ caller[0] + file, line_number = $1, $2.to_i + else # We can't give good trackback info; fallback to this line: + file, line_number = __FILE__, __LINE__ + end + + # We use eval rather than building proc's because it allows us to avoid + # linking the Hash's to this method's binding. Experience has shown that + # doing so can cause obtuse memory leaks. + class_eval code, file, line_number + end + end + + end +end diff --git a/activesupport/test/caching_tools_test.rb b/activesupport/test/caching_tools_test.rb new file mode 100644 index 0000000000..9f3e42e496 --- /dev/null +++ b/activesupport/test/caching_tools_test.rb @@ -0,0 +1,81 @@ +require 'test/unit' +require File.dirname(__FILE__)+'/../lib/active_support/caching_tools' + +class HashCachingTests < Test::Unit::TestCase + + def cached(&proc) + return @cached if @cached + + @cached_class = Class.new(&proc) + @cached_class.class_eval do + extend ActiveSupport::CachingTools::HashCaching + hash_cache :slow_method + end + @cached = @cached_class.new + end + + def test_cache_access_should_call_method + cached do + def slow_method(a) raise "I should be here: #{a}"; end + end + assert_raises(RuntimeError) { cached.slow_method_cache[1] } + end + + def test_cache_access_should_actually_cache + cached do + def slow_method(a) + (@x ||= []) + if @x.include?(a) then raise "Called twice for #{a}!" + else + @x << a + a + 1 + end + end + end + assert_equal 11, cached.slow_method_cache[10] + assert_equal 12, cached.slow_method_cache[11] + assert_equal 11, cached.slow_method_cache[10] + assert_equal 12, cached.slow_method_cache[11] + end + + def test_cache_should_be_clearable + cached do + def slow_method(a) + @x ||= 0 + @x += 1 + end + end + assert_equal 1, cached.slow_method_cache[:a] + assert_equal 2, cached.slow_method_cache[:b] + assert_equal 3, cached.slow_method_cache[:c] + + assert_equal 1, cached.slow_method_cache[:a] + assert_equal 2, cached.slow_method_cache[:b] + assert_equal 3, cached.slow_method_cache[:c] + + cached.slow_method_cache.clear + + assert_equal 4, cached.slow_method_cache[:a] + assert_equal 5, cached.slow_method_cache[:b] + assert_equal 6, cached.slow_method_cache[:c] + end + + def test_deep_caches_should_work_too + cached do + def slow_method(a, b, c) + a + b + c + end + end + assert_equal 3, cached.slow_method_cache[1][1][1] + assert_equal 7, cached.slow_method_cache[1][2][4] + assert_equal 7, cached.slow_method_cache[1][2][4] + assert_equal 7, cached.slow_method_cache[4][2][1] + + assert_equal({ + 1 => {1 => {1 => 3}, 2 => {4 => 7}}, + 4 => {2 => {1 => 7}}}, + cached.slow_method_cache + ) + end + +end -- cgit v1.2.3