微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

Ruby Enumerators 链接究竟是如何工作的?

如何解决Ruby Enumerators 链接究竟是如何工作的?

考虑以下代码

[1,2,3].map.with_index { |x,i| x * i }
# => [0,6]

这究竟是如何工作的?

我对 map 的心智模型是它迭代并在每个元素上应用一个函数with_index 是否以某种方式将函数传递给枚举器 [1,3].map在这种情况下,该函数是什么?

这个 SO thread 显示了枚举器如何传递数据,但没有回答问题。实际上,如果您将 map 替换为 each,则行为会有所不同:

[1,3].each.with_index { |x,i| x * i }
# => [1,3]

map 似乎携带了必须应用函数的信息,除了携带要迭代的数据之外。这是如何运作的?

解决方法

Todd 的回答非常好,但我觉得看到更多 Ruby 代码可能会有所帮助。具体来说,让我们尝试在 each mapArray

我不会直接使用任何 EnumerableEnumerator 方法,所以我们会看到它是如何在幕后工作的(我仍然会使用 for 循环,并且那些技术上调用#each 在幕后,但这只是作弊)

首先,有 eacheach 很简单。它遍历数组并对每个元素应用一个函数,然后返回原始数组。

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

很简单。现在如果我们不传递一个块怎么办?让我们对 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。我们可以看到这两个函数实际上在数组上表现得像 mapeach do

my_each([1,i| x * i } # [1,3]
my_map ([1,i| x * i } # [0,6]

所以你的直觉很准

map 似乎携带了必须应用函数的信息,除了携带要迭代的数据之外。这是如何运作的?

这正是它的作用。 map 创建一个 Enumerator,它的块知道在最后调用 map,而 each 做同样的事情,但使用 each。当然,实际上,出于效率和引导的原因,所有这些都是用 C 实现的,但基本思想仍然存在。

,

在没有块的情况下使用 Array#map 只会返回一个枚举器,然后将每个元素提供给 Enumerator#with_index,块的结果作为集合返回。它并不复杂,类似于(但可能比)以下代码。使用 Ruby 3.0.1:

results = []
[1,3].each_with_index { results << _1 * _2 }
results
#=> [0,6]

使用 Array#each 不会从块中返回一个集合。它只返回 self 或另一个枚举器,因此预期行为因设计而异。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。