【问题标题】:What's the proper idiom for short-circuiting a Ruby `begin ... end` block?短路 Ruby `begin ... end` 块的正确习惯用法是什么?
【发布时间】:2017-08-13 11:12:13
【问题描述】:

我经常使用 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之外,还有更好的成语吗?

【问题讨论】:

  • 一篇关于一些约定的好文章:justinweiss.com/articles/…
  • 强烈建议不要使用全局变量进行缓存。保持变量上下文相关很重要,即尽可能使用@ 样式的实例变量。
  • 如果你的程序有纯函数,你可以更进一步,缓存方法调用自己。例如cache-method。为缓存提供离散 API 的一个好处是,如果需要,可以更轻松地切换持久性类型。

标签: ruby memoization


【解决方案1】:

我会添加另一层:

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】:

    据我所知,begin...end 不能短路。不过,您可以完全按照您的想法使用 procs:

    $memo = {}
    def calculate(something)
      $memo[something] ||= -> do
        return 'foo' if something == 'bar'
        perform_calculation(something)
      end.call
    end
    

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

    【讨论】:

      【解决方案3】:

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

      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"
      

      【讨论】:

      • 非常有趣!大概是在要包含的模块中。
      • @CarySwoveland 这是Rails 中旧的memoize 方法的重演。 RIP。
      【解决方案4】:

      我担心我没有正确理解这个问题,因为它看起来很简单。

      $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"} 
      

      【讨论】:

        【解决方案5】:

        我不知道使用return 的解决方案,但对于您示例中的保护子句,我将使用case

        $memo = {}
        def calculate(something)
          $memo[something] ||= case something
                               when 'foo' then 'bar'
                               else perform_calculation(something)
                               end
        
        end
        

        【讨论】:

          【解决方案6】:

          您可以在 #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
          

          【讨论】:

            猜你喜欢
            • 2016-04-10
            • 2012-02-05
            • 2010-12-30
            • 2013-12-06
            • 2020-10-14
            • 2012-02-02
            相关资源
            最近更新 更多