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
初始代码中需要的是:
- 它 returns
nil
如果块从不取真值;
- 它return是区块第一次为真时的值;
- 找到第一个匹配项后停止;
- 它不会再次调用
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)
一次 。
缺点:
- a bit more complicated 正是这种方法在幕后的工作原理...
扩展 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 方法过早 return
s 如果找到真值。
- 您只在 "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++
、Java
、Rust
、Scala
、...)中,上述问题不是问题.
就我个人而言,我认为使用惰性枚举器是最优雅、最通用的解决方案。但我想包括一些替代品以供比较。
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)}
您可以简单地使用 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
在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
初始代码中需要的是:
- 它 returns
nil
如果块从不取真值; - 它return是区块第一次为真时的值;
- 找到第一个匹配项后停止;
- 它不会再次调用
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)
一次 。
缺点:
- a bit more complicated 正是这种方法在幕后的工作原理...
扩展 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 方法过早return
s 如果找到真值。 - 您只在 "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++
、Java
、Rust
、Scala
、...)中,上述问题不是问题.
就我个人而言,我认为使用惰性枚举器是最优雅、最通用的解决方案。但我想包括一些替代品以供比较。
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)}
您可以简单地使用 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