Ruby 枚举器链接究竟是如何工作的?
How does Ruby Enumerators chaining work exactly?
考虑以下代码:
[1,2,3].map.with_index { |x, i| x * i }
# => [0,2,6]
这究竟是如何工作的?
我对 map
的心智模型是它迭代并在每个元素上应用一个函数。 with_index
是否以某种方式将函数传递给枚举器 [1,2,3].map
,在这种情况下该函数是什么?
这个 SO thread 显示了枚举器如何传递数据,但没有回答问题。实际上,如果将 map
替换为 each
,则行为会有所不同:
[1,2,3].each.with_index { |x, i| x * i }
# => [1,2,3]
map
似乎携带了必须应用函数的信息,除了携带要迭代的数据之外。它是如何工作的?
使用 Array#map without a block simply returns an Enumerator, where each element is then fed to Enumerator#with_index 块的结果被 return 编辑为一个集合。它并不复杂,并且类似于(但可能比)以下代码。使用 Ruby 3.0.1:
results = []
[1, 2, 3].each_with_index { results << _1 * _2 }
results
#=> [0, 2, 6]
使用 Array#each 不会 return 块中的集合。它只是 return 自身或另一个枚举器,因此预期行为在设计上有所不同。
Todd 的回答非常好,但我觉得多看一些 Ruby 代码可能会有所帮助。具体来说,让我们自己尝试在 Array
上 编写 each
和 map
。
我不会直接使用任何 Enumerable
或 Enumerator
方法,所以我们看看它是如何在引擎盖下工作的(我仍然会使用 for
循环,以及那些技术上在幕后调用 #each
,但这只是作弊而已)
首先是 each
。 each
很简单。它遍历数组并对每个元素应用一个函数,然后返回 原始 数组。
def my_each(arr, &block)
for i in 0..arr.length-1
block[arr[i]]
end
arr
end
很简单。现在,如果我们不通过一个街区怎么办。让我们稍微改变一下以支持它。我们实际上想要 延迟 执行 each
的行为以允许 Enumerator
执行其操作
def my_each(arr, &block)
if block
for i in 0..arr.length-1
block[arr[i]]
end
arr
else
Enumerator.new do |y|
my_each(arr) { |*x| y.yield(*x) }
end
end
end
因此,如果我们不传递块,我们会创建一个 Enumerator
,在使用时调用 my_each
,使用枚举器 yield 对象作为块。 y
对象是一件有趣的事情,但您可以将其视为基本上是您最终将传入的块。因此,在
my_each([1, 2, 3]).with_index { |x, i| x * i }
将 y
视为 { |x, i| x * i }
位。它比那更复杂一点,但就是这个想法。
顺便说一句,在 Ruby 2.7 及更高版本中,Enumerator::Yielder
对象有自己的 #to_proc
,因此如果您使用的是最新的 Ruby 版本,您可以只是做
Enumerator.new do |y|
my_each(arr, &y)
end
而不是
Enumerator.new do |y|
my_each(arr) { |*x| y.yield(*x) }
end
现在让我们将这种方法扩展到 map
。用块写 map
很容易。就像 each
但我们累积结果。
def my_map(arr, &block)
result = []
for i in 0..arr.length-1
result << block[arr[i]]
end
result
end
很简单。现在如果我们不通过一个街区怎么办?让我们做 exact 与 my_each
相同的事情。也就是说,我们只是要创建一个 Enumerator
,然后在 Enumerator
中调用 my_map
.
def my_map(arr, &block)
if block
result = []
for i in 0..arr.length-1
result << block[arr[i]]
end
result
else
Enumerator.new do |y|
my_map(arr) { |*x| y.yield(*x) }
end
end
end
现在,Enumerator
知道,无论何时它最终得到一个块,它都会在最后对该块使用 my_map
。我们可以看到这两个函数在数组上的实际行为类似于 map
和 each
do
my_each([1, 2, 3]).with_index { |x, i| x * i } # [1, 2, 3]
my_map ([1, 2, 3]).with_index { |x, i| x * i } # [0, 2, 6]
所以你的直觉是正确的
map
seems to carry the information that a function has to be applied, on top of carrying the data to iterate over. How does that work?
这正是它的作用。 map
创建一个 Enumerator
其块知道在末尾调用 map
,而 each
做同样的事情但使用 each
。当然,实际上,出于效率和引导的原因,所有这些都是用 C 实现的,但基本思想仍然存在。
考虑以下代码:
[1,2,3].map.with_index { |x, i| x * i }
# => [0,2,6]
这究竟是如何工作的?
我对 map
的心智模型是它迭代并在每个元素上应用一个函数。 with_index
是否以某种方式将函数传递给枚举器 [1,2,3].map
,在这种情况下该函数是什么?
这个 SO thread 显示了枚举器如何传递数据,但没有回答问题。实际上,如果将 map
替换为 each
,则行为会有所不同:
[1,2,3].each.with_index { |x, i| x * i }
# => [1,2,3]
map
似乎携带了必须应用函数的信息,除了携带要迭代的数据之外。它是如何工作的?
使用 Array#map without a block simply returns an Enumerator, where each element is then fed to Enumerator#with_index 块的结果被 return 编辑为一个集合。它并不复杂,并且类似于(但可能比)以下代码。使用 Ruby 3.0.1:
results = []
[1, 2, 3].each_with_index { results << _1 * _2 }
results
#=> [0, 2, 6]
使用 Array#each 不会 return 块中的集合。它只是 return 自身或另一个枚举器,因此预期行为在设计上有所不同。
Todd 的回答非常好,但我觉得多看一些 Ruby 代码可能会有所帮助。具体来说,让我们自己尝试在 Array
上 编写 each
和 map
。
我不会直接使用任何 Enumerable
或 Enumerator
方法,所以我们看看它是如何在引擎盖下工作的(我仍然会使用 for
循环,以及那些技术上在幕后调用 #each
,但这只是作弊而已)
首先是 each
。 each
很简单。它遍历数组并对每个元素应用一个函数,然后返回 原始 数组。
def my_each(arr, &block)
for i in 0..arr.length-1
block[arr[i]]
end
arr
end
很简单。现在,如果我们不通过一个街区怎么办。让我们稍微改变一下以支持它。我们实际上想要 延迟 执行 each
的行为以允许 Enumerator
执行其操作
def my_each(arr, &block)
if block
for i in 0..arr.length-1
block[arr[i]]
end
arr
else
Enumerator.new do |y|
my_each(arr) { |*x| y.yield(*x) }
end
end
end
因此,如果我们不传递块,我们会创建一个 Enumerator
,在使用时调用 my_each
,使用枚举器 yield 对象作为块。 y
对象是一件有趣的事情,但您可以将其视为基本上是您最终将传入的块。因此,在
my_each([1, 2, 3]).with_index { |x, i| x * i }
将 y
视为 { |x, i| x * i }
位。它比那更复杂一点,但就是这个想法。
顺便说一句,在 Ruby 2.7 及更高版本中,Enumerator::Yielder
对象有自己的 #to_proc
,因此如果您使用的是最新的 Ruby 版本,您可以只是做
Enumerator.new do |y|
my_each(arr, &y)
end
而不是
Enumerator.new do |y|
my_each(arr) { |*x| y.yield(*x) }
end
现在让我们将这种方法扩展到 map
。用块写 map
很容易。就像 each
但我们累积结果。
def my_map(arr, &block)
result = []
for i in 0..arr.length-1
result << block[arr[i]]
end
result
end
很简单。现在如果我们不通过一个街区怎么办?让我们做 exact 与 my_each
相同的事情。也就是说,我们只是要创建一个 Enumerator
,然后在 Enumerator
中调用 my_map
.
def my_map(arr, &block)
if block
result = []
for i in 0..arr.length-1
result << block[arr[i]]
end
result
else
Enumerator.new do |y|
my_map(arr) { |*x| y.yield(*x) }
end
end
end
现在,Enumerator
知道,无论何时它最终得到一个块,它都会在最后对该块使用 my_map
。我们可以看到这两个函数在数组上的实际行为类似于 map
和 each
do
my_each([1, 2, 3]).with_index { |x, i| x * i } # [1, 2, 3]
my_map ([1, 2, 3]).with_index { |x, i| x * i } # [0, 2, 6]
所以你的直觉是正确的
map
seems to carry the information that a function has to be applied, on top of carrying the data to iterate over. How does that work?
这正是它的作用。 map
创建一个 Enumerator
其块知道在末尾调用 map
,而 each
做同样的事情但使用 each
。当然,实际上,出于效率和引导的原因,所有这些都是用 C 实现的,但基本思想仍然存在。