Hi,
In the "Class and Module Definitions" section of Chapter 19
in Pickaxe 1, there's code for a method called ExampleDate.once.
I've tried to generalize it in order to get return value
caching that works for any method - provided that the cached
method follows some rules:
- It must return a value based only on the values of the
arguments it receives.
- It can not have any side effects (such as changing some state
or doing any I/O operations).
You call cacheable or cacheable_singleton in the class where the
methods to cache are defined, a bit like private or attr_reader
and such. I put the "cacheable..." methods in Module at first,
but then they wouldn't work with methods on the outermost level.
What do you think? Is this technique fool-proof? Can it be
optimized? Any other comments?
I'm including an example of one case where caching works
beautifully. It's an implementation of the Fibonacci series using
recursion.
--Jonas
#---------------------- cacheable.rb ------------------------------
def cacheable_def(*ids)
ids.each { |id|
orig_method = cache = "__#{id.to_i}__"
yield <<-end_code
alias #{orig_method} #{id}
def #{id}(*args, &block)
def #{id}(*args, &block)
key = args.inspect + "\#{block}"
return @#{cache}[key] if @#{cache}.has_key? key
@#{cache}[key] = #{orig_method}(*args, &block)
end
@#{cache} = {}
self.#{id}(*args, &block)
end
END { puts "Cached \#{@#{cache}.size} values for #{id}" } if
$DEBUG
end_code
}
end
def cacheable(*ids)
cacheable_def(*ids) { |code| class_eval code }
end
def cacheable_singleton(*ids)
cacheable_def(*ids) { |code| instance_eval code }
end
#------------------------- fib.rb ------------------------------------
require 'benchmark'
require 'cacheable'
def fib(n)
n < 2 ? n : fib(n - 2) + fib(n - 1)
end
max = 30
results = []
Benchmark.bm(7) do |x|
x.report('normal:') { results << fib(max) }
cacheable_singleton :fib
x.report('cached:') { results << fib(max) }
end
p results