Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the proper idiom for short-circuiting a Ruby `begin ... end` block?

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:

  1. it's easy to forget I'm not allowed to use return in this case.
  2. with many conditions it clutters up the code as opposed to the guard clause.

Is there a better idiom for this besides just avoiding return?

like image 499
Sasgorilla Avatar asked Mar 21 '17 16:03

Sasgorilla


3 Answers

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.

like image 198
eiko Avatar answered Oct 10 '22 07:10

eiko


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.

like image 20
kaikuchn Avatar answered Oct 10 '22 06:10

kaikuchn


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"
like image 42
tadman Avatar answered Oct 10 '22 07:10

tadman