Ruby 可枚举:块的第一个真值

Ruby Enumerable: first truthy value of a block

在ruby中我们可以做这样的事情:

stuff_in_trash.detect(&:eatable?)
=> :pack_of_peanuts

stuff_in_trash.detect(&:drinkable?)
=> nil

但是,如果我们感兴趣的是块第一次为真时的值,而不是块为其取真值的第一个项目呢?

也就是转换如下代码:

def try_to_make_artwork_from(enumerable)
  enumerable.each do |item|
    result = make_artwork_from item
    return result if result
 end
   nil
end

类似于:

def try_to_make_artwork_from(enumerable)
  enumerable.try_with { |item| make_artwork_from item }
end

初始代码中需要的是:

  1. 它 returns nil 如果块从不取真值;
  2. 它return是区块第一次为真时的值;
  3. 找到第一个匹配项后停止;
  4. 它不会再次调用 make_artwork_from(假设它不能保证 return 下次调用时会得到相同的结果)。

不尽如人意的是result用了三遍,却与故事无关

编辑:抱歉,最初的实现不正确,它需要 return nil 以防块值永远不会为真。

enumerable.lazy.map(&:block).detect(&:itself)

可以,但这是最简单的方法吗?与简单地使用 each 并缓存值相比,它是否有效?

您可以使用所需的迭代器扩展 Enumerable:

module Enumerable
  def detect_return
    self.detect do |i|
      r = yield(i) and return r
    end
  end
end

[1, 2, 3].detect_return do |i|
   if i + 1 >= 2
     puts "I will be printed only once"
     "ok, here I am"
   end
end
# I will be printed only once
# => "ok, here I am"

就我们都认为猴子补丁是个坏东西而言,让我们提供危害较小的变体:

def detect_return(enumerable)
  enumerable.detect do |i|
    r = yield(i) and return r
  end
end

detect_return([1, 2, 3]) do |i|
  if i + 1 >= 2
    puts "I will be printed only once"
    "ok, here I am"
  end
end
# I will be printed only once
# => "ok, here I am"

detect_return([1, 2, 3]) do |i|
  if i + 1 >= 42
    puts "I will be printed only once"
    "ok, here I am"
  end
end
# => nil

调用方法两次

查看 stuff_in_trash.detect(&:eatable?) 的第一个示例,您可以执行以下操作:

stuff_in_trash.detect(&:eatable?)&.eatable?

请注意 safe navigation operator (&.) 的使用,自 ruby v2.3+ 起可用,它涵盖了 detect 返回 nil.

优点:

  • 您没有遍历整个 stuff_in_trash 列表,因为 detect? 方法在第一个真实项目处停止。

缺点:

  • 您在 truthy 对象上调用了 eatable? 方法两次。 (可能的性能问题,通常是不好的做法。)
  • 代码可以得到ugly/confusing;特别是如果您在 detect 块中应用的方法更加复杂。例如:make_artwork_from( items.detect {|item| make_item_from(item)} - 这甚至没有涵盖 detect 返回 nil 的可能问题!

使用惰性枚举器

查看 make_artwork_from(item) 的第二个示例,您可以执行以下操作:

items.lazy.map {|item| make_artwork_from(item)}.detect(&:itself)

优点:

  • 您没有遍历完整的 items 列表,因为 lazy 枚举器查询 "minimum" 项数来计算最终方法链的结果.
  • 您只在 "truthy" 对象上调用 make_artwork_from(item) 一次

缺点:

扩展 Enumerable class

不言自明 - 您可以定义如下方法:

module Enumerable
  def detect_result
    self.detect do |i|
      if result = yield(i)
        return result
      end
    end
  end
end

# Usage:
items.detect_result { |item| make_artwork_from(item) }

优点:

  • 您没有遍历完整的 items 列表,因为扩展 class 方法过早 returns 如果找到真值。
  • 您只在 "truthy" 对象上调用 make_artwork_from(item) 一次

缺点:

  • 像这样全局扩展核心 classes 通常不是一个好主意!您可以考虑将其改为 refinement,尽管这些并未被广泛使用。

写成函数,而不是方法

...我的意思是,将 Enumerable 对象作为方法参数传递,而不是调用对象上的方法。与上面类似,但实现方式如下:

def detect_result(enumerable)
  enumerable.detect do |i|
    if result = yield(i)
      return result
    end
  end
end

# Usage:
detect_result(items) { |item| make_artwork_from(item) }

优点:

同上

缺点:

  • 这不是很面向对象;所以可以说不是 "the ruby way" 来处理这个。没有什么可以阻止您将不可枚举的对象传递给 detect_result,这可能会导致运行时错误!
  • 在其他静态类型的语言(C++JavaRustScala、...)中,上述问题不是问题.

就我个人而言,我认为使用惰性枚举器是最优雅、最通用的解决方案。但我想包括一些替代品以供比较。

it does the job, but is the simplest way? Is it efficient compare to simply using a each and caching the value?

最简单的方法?

我们可以定义这个方法:

def first_truthy_block(enumerable, &block)
  enumerable.lazy.map(&block).find(&:itself)
end

实际操作:

array = [0,1,2,3,4,:x5,'abc']

puts first_truthy_block(array) { |x|
  if x ** 2 > 10 then
    "ARTWORK with #{x}!!!"
  end
} 
#=> ARTWORK with 4!!!

能再简单点吗?

  • enumerable 是必需的,这是您正在处理的对象。
  • 需要
  • lazy,不会尽快停止,会抛出异常:x5**2.
  • 需要地图,您需要对您的元素应用一些方法
  • 需要 find 最多从您的可枚举中提取一个值

使用标准 Enumerable 方法,我看不出它还能更简单。

效率高吗?

它比您的 each 方法慢。它基本上做同样的事情并且应该具有相同的复杂性但是它确实使用了更多的方法调用并创建了更多的对象:

require 'fruity'

def first_truthy_block_lazy(enumerable, &block)
  enumerable.lazy.map(&block).find(&:itself)
end

def first_truthy_block_each(enumerable, &block)
  enumerable.each do |item|
    result = block.call(item)
    return result if result
 end
   nil
end

big_array = Array.new(10_000){rand(4)} + [5] + Array.new(10_000){rand(20)} + [:x, :y, 'z']

compare do
  _lazy_map do
    first_truthy_block_lazy(big_array) { |x|
      if x ** 2 > 10 then
        "ARTWORK with #{x}!!!"
      end
    }
  end

  _each do       
    first_truthy_block_each(big_array) { |x|
      if x ** 2 > 10 then
        "ARTWORK with #{x}!!!"
      end
    }
  end
end

果味 returns :

Running each test once. Test will take about 1 second.
_each is faster than _lazy_map by 3x ± 0.1
array.inject(nil) {|c,v| c or m(v)}

(来自 https://makandracards.com/makandra/35569-how-to-iterate-over-an-enumerable-returning-the-first-truthy-result-of-a-block-map-find

您可以简单地使用 break:

# "pseudo" code
my_enum.find do |item|
  calculated_result = begin
    # some stuf
  end

  break calculated_result if my_condition_is_true
end


# Or from the given link exemple 
first_image_url = posts.find do |post|
  break post.image.url if post.image.present?
end