如何在 Ruby 的 `begin ... end` 块中进行短路处理?

7
我通常使用begin ... end块语法来进行Ruby方法的记忆化:
$memo = {}
def calculate(something)
  $memo[something] ||= begin
    perform_calculation(something)
  end
end

然而,这里有一个陷阱。如果我通过一个守卫子句从 begin ... end 块中提前返回,结果将不会被记忆化:
$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

我可以通过避免使用return语句来避免这个问题:
$memo = {}
def calculate(something)
  $memo[something] ||= begin
    if something == 'bar'
      'foo'
    else
      perform_calculation(something)
    end
  end
end

这个方法可以工作,但我不是很喜欢它,因为:

  1. 很容易忘记在这种情况下不允许使用 return
  2. 当有多个条件时,与保护条款相比,它会使代码变得混乱。

除了避免使用 return,还有更好的惯用语吗?


一篇关于一些规范的好文章:http://www.justinweiss.com/articles/4-simple-memoization-patterns-in-ruby-and-one-gem// - Anthony
1
我强烈建议不要使用全局变量来缓存。保持您的变量上下文有关,尽可能使用@风格的实例变量。 - tadman
如果你的程序具有纯函数,你可以更进一步地缓存方法调用本身。例如cache-method。拥有一个离散的缓存API的好处是,如果需要,更容易切换持久性类型。 - max pleaner
6个回答

6
据我所知,begin...end不能被短路。但是您可以使用procs来实现您尝试做的事情:
$memo = {}
def calculate(something)
  $memo[something] ||= -> do
    return 'foo' if something == 'bar'
    perform_calculation(something)
  end.call
end

话虽如此,但我从未见过这样做的方式,所以肯定不是惯用语。


4
我会添加另一层:

我会添加另一层:

def calculate(something)
  $memo[something] ||= _calculate(something)
end

def _calculate(something)
  return if something == 'bar'
  perform_calculation(something) # or maybe inline this then
end

这有另一个好处,就是提供了一个方法,你可以在任何时候调用它,以确保获得最新计算结果。但我认为方法命名还需要再思考一下。

2

解决这个问题的一种方法是使用元编程,在定义方法后对其进行包装。这样可以保留其中的任何行为:

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

这会创建一个闭包变量,它充当缓存。这避免了丑陋的全局变量,但也意味着如果需要清除缓存,您实际上无法清除它,因此,如果传入大量不同的参数,它可能会消耗大量内存。要小心!如果需要,可以通过为任何给定方法x定义一些辅助方法(如x_forget)来添加该功能。

以下是其工作原理:

def calculate(n)
  return n if (n < 1)

  n + 2
end
memoize(:calculate)

然后你会看到:

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"

非常有趣!可能是在一个要被包含的模块中。 - Cary Swoveland
@CarySwoveland 这是一个重新设计的旧版memoize方法,它曾经在Rails中使用。安息吧。 - tadman

0
你可以在 #tap 块中使用 break 语句:
def val
  @val ||= default.tap do
    break val1 if cond1
    break val2 if cond2
    break val3 if cond3
  end
end

你也可以使用1.6的#then或1.5的#yield_self,但是别忘了在块的末尾返回默认值,否则它会默认为nil。在你的例子中这不是问题,因为默认值只在最后被评估:

def calculate(something)
  @calculate ||= {}
  return @calculate[something] if @calculate.key?(something)

  @calculate[something] = something.then do |something|
    break 'foo' if something == 'bar'
    perform_calculation(something)
  end
end

0
我不知道用return的解决方法,但是针对你示例中的守卫子句,我会使用case
$memo = {}
def calculate(something)
  $memo[something] ||= case something
                       when 'foo' then 'bar'
                       else perform_calculation(something)
                       end

end

0

恐怕我没有正确理解问题,因为似乎应该使用相当简单的方法。

$memo = {}

def calculate(something)
  $memo[something] ||= something == 'bar' ? 'foo' : perform_calculation(something)
end

让我们来试一下。

def perform_calculation(something)
  'baz'
end

calculate('bar')
  #=> "foo" 
$memo
  #=> {"bar"=>"foo"} 
calculate('baz')
  #=> "baz" 
$memo
  #=> {"bar"=>"foo", "baz"=>"baz"} 
calculate('bar')
  #=> "foo" 
$memo
  #=> {"bar"=>"foo", "baz"=>"baz"} 

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接