I often memoize Ruby methods using the begin ... end
block syntax:
$memo = {}
def calculate(something)
$memo[something] ||= begin
perform_calculation(something)
end
end
However, there's a gotcha here. If I return early from the begin ... end
block via a guard clause, the result is not memoized:
$memo = {}
def calculate(something)
$memo[something] ||= begin
return 'foo' if something == 'bar'
perform_calculation(something)
end
end
# does not memoize 'bar'; method will be run each time
I can avoid this by avoiding the return
statement:
$memo = {}
def calculate(something)
$memo[something] ||= begin
if something == 'bar'
'foo'
else
perform_calculation(something)
end
end
end
This works, but I don't love it because:
return
in this case.Is there a better idiom for this besides just avoiding return
?
As far as I know, begin...end cannot be short-circuited. You can do exactly what you're trying to do with procs though:
$memo = {}
def calculate(something)
$memo[something] ||= -> do
return 'foo' if something == 'bar'
perform_calculation(something)
end.call
end
That being said, I've never seen this done before, so it's certainly not idiomatic.
I'd add another layer:
def calculate(something)
$memo[something] ||= _calculate(something)
end
def _calculate(something)
return if something == 'bar'
perform_calculation(something) # or maybe inline this then
end
This has the additional benefit of providing you with a method you could call whenever you want to be sure to get a freshly computed result. I would spend some more time on the method naming though.
One way to tackle this is with meta-programming where you wrap the method after it's defined. This preserves any behaviour in it:
def memoize(method_name)
implementation = method(method_name)
cache = Hash.new do |h, k|
h[k] = implementation.call(*k)
end
define_method(method_name) do |*args|
cache[args]
end
end
This creates a closure variable which acts as a cache. That avoids the ugly global, but it also means you can't really clear out that cache if you need to, so if you pass in a large number of different arguments it could end up consuming a lot of memory. Be cautious! That functionality could be added if necessary by defining some auxiliary method like x_forget
for any given method x
.
Here's how it works:
def calculate(n)
return n if (n < 1)
n + 2
end
memoize(:calculate)
Then you can see:
10.times do |i|
p '%d=%d' % [ i % 5, calculate(i % 5) ]
end
# => "0=0"
# => "1=3"
# => "2=4"
# => "3=5"
# => "4=6"
# => "0=0"
# => "1=3"
# => "2=4"
# => "3=5"
# => "4=6"
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With