为啥 Enumerator 包含 Enumerable

Posted

技术标签:

【中文标题】为啥 Enumerator 包含 Enumerable【英文标题】:why does Enumerator include Enumerable为什么 Enumerator 包含 Enumerable 【发布时间】:2013-04-03 00:34:03 【问题描述】:

挖这个,这是一个很酷的Enumerator(惰性序列)从1到(最大的Float Ruby 可以表示):

1.9.3-p327 :014 > e = (1..Float::INFINITY).each

看看我们如何抓取序列的前面:

1.9.3-p327 :015 > e.first
 => 1 
1.9.3-p327 :016 > e.take(2)
 => [1, 2] 

这是好东西吧?我也这么认为。但接下来是:

1.9.3-p327 :017 > e.drop(2).first

进入拉拉地。我的意思是它不会在 5 秒内返回。

哦,这里有个线索:

1.9.3-p327 :020 > p e.method(:drop)
#<Method: Enumerator(Enumerable)#drop>

似乎 Enumerator (e) 将 #drop 方法从 Enumerable(模块)混合到 Enumerator(类)中。现在你问为什么 Ruby 会把Enumerable 混合到Enumerator 中?我不知道。但它确实存在,记录在 Enumerator in Ruby 1.9.3 和 Enumerator in Ruby 2.0 中。

我看到的问题是Enumerable 中定义的某些方法或Enumerator 上的某种工作。示例包括 #first#take。至少还有一个:#drop 不起作用。

在我看来 Enumerator 包括 Enumerable 是一个错误。你怎么看?

PS 注意 Ruby 2.0 定义了 Enumerator::LazyEnumerator 的子类),它定义了一堆 Enumerable 方法总是惰性的。这里有一股腥味。为什么混合非惰性和在某些情况下损坏的方法(到Enumerator)只是为了转身并在子类(Enumerator)中提供惰性替代方案?

另见:

1.9.3-p327 :018 > p e.method(:first)
#<Method: Enumerator(Enumerable)#first>
1.9.3-p327 :020 > p e.method(:drop)
#<Method: Enumerator(Enumerable)#drop>

【问题讨论】:

请注意,each 中的 e = (1..Float::INFINITY).each 没有区别。您应该将其排除在外,或者将其替换为 lazy,具体取决于您到底想要什么。 谢谢马克-安德烈。但是 Ruby 1.9.3 中没有 Enumerable#lazy。这仅在 2.0 中可用。我认为我的主要误解之一是假设 #drop 根本就返回了一个枚举器。出于某种原因,虽然 #drop 和 #take 都有可能实现微不足道的 Enumerator ,但它们都不会返回 Enumerator !即使是Ruby Bug #7715 "Lazy enumerators should want to stay lazy" 的修复程序也无法解决这些问题。想想看,这些不能“修复”,因为这样做会破坏依赖于返回数组的代码! 确实,drop 是“渴望”。顺便说一句,您可以在任何版本的 Ruby 中使用lazy 使用require 'backports/2.0.0/enumerable/lazy'require 'backports/2.0.0/enumerable/lazy' 【参考方案1】:

这也是许多其他集合框架共有的设计选择。

Ruby 的集合操作不是类型保留的。他们总是返回一个Array,不管他们被调用的是什么类型的集合。这也是.NET 所做的,除了类型总是IEnumerable,这两者都更有用(因为更多的东西可以表示为IEnumerable 而不是Array,例如无限序列)和同时也不太好用(因为IEnumerable的接口比Array小很多,所以可以对它做的操作也比较少)。

这允许 Ruby 的收集操作实现一次,无需重复。

这也意味着将自己的集合集成到 Ruby 的集合框架中非常容易:只需实现 each、mixin Enumerable 即可。如果 Ruby 的未来版本添加了新的收集方法(例如 Ruby 1.9 中的 flat_map),您无需执行任何操作,它也适用于您的集合。

另一个设计选择是使所有收集操作保持类型。所以,所有的集合操作都会返回它们被调用的类型。

有些语言可以做到这一点。但是,它是通过将所有集合方法复制并粘贴到所有集合类中来实现的,即具有大量代码重复。

这意味着如果你想将你自己的集合添加到集合框架中,你必须实现集合协议的每一个方法。如果未来版本的语言添加了新方法,那么您必须发布新版本的集合。

Scala 2.8 的集合框架是第一次有人想出如何在不重复代码的情况下进行类型保留的集合操作。但那是在设计 Ruby 的集合框架之后很久。在设计 Ruby 的集合框架时,根本不知道如何在不重复代码的情况下进行类型保留的集合操作,因此 Ruby 的设计者选择反对重复。

从 Ruby 1.9 开始,实际上存在一些重复。一些Hash 方法被复制以返回Hashes 而不是Arrays。而且您已经提到了 Ruby 2.0 的 Enumerator::Lazy,它复制了许多 Enumerable 方法以返回 Enumerator::Lazy

可以使用 Scala 在 Ruby 中使用的相同技巧,但需要对集合框架进行彻底改造,这将使每个现有的集合实现都过时。 Scala 能够做到这一点是因为当时几乎没有任何用户群。

【讨论】:

像往常一样,很好的答案。我不会说“他们总是返回Array”,因为也可以返回Enumerators。 Lazy 部分有点误导。懒惰的方法是专门的,其他的不是。另一方面,Lazy#to_enum 是专门化的,这个巧妙的技巧意味着任何返回EnumeratorEnumerable 方法在Lazy 上调用时都将返回Enumerator::Lazy,无需专门化。 bugs.ruby-lang.org/issues/7715 中的详细信息【参考方案2】:

回应第一部分:

“进入拉拉之地。我的意思是它不会在更少的时间内返回 超过 5 秒。”

这种行为似乎与那些方法应该做的一致:

take(n) → array # Returns first n elements from enum.

这意味着您只需要迭代到 N 即可返回它。

drop(n) → array # #Drops first n elements from enum, and returns rest elements in an array.

这意味着它需要其余元素才能返回它们。并且由于您的上限是Float::INFINITY,它的行为就是这样。

来源:Enumerable

【讨论】:

感谢 fmendez。是的,我完全假设(错误地)#take 和 #drop 会返回 Enumerators。现在我明白为什么那会很糟糕了。很多代码依赖于那些返回数组。 本质上,任何不占用块的旧(1.9 之前)Enumerable 方法都无法“升级”以返回 Enumerator,因为调用者无法表示它想要返回 Enumerator . Enumerable#take 和 #drop 不占用块,因此它们不能返回 Enumerators。当然 Enumerator::Lazy#take 和 #drop 确实会按预期返回 Enumerators。

以上是关于为啥 Enumerator 包含 Enumerable的主要内容,如果未能解决你的问题,请参考以下文章

如何用 Enumerator 的特定子类包装数组?

“未定义的方法`enumerable_enumerator_path'”错误

Ruby Enumerator的各种迭代

在 C# 中获取 Enumerator.Current 的索引 [重复]

当你的类没有定义 #each 时,返回 Enumerator::Lazy 的最佳方法是啥?

C#的枚举数(Enumerator)和可枚举类型(Enumerable)