为啥 Set#map 返回一个 Array 而不是另一个 Set?
Posted
技术标签:
【中文标题】为啥 Set#map 返回一个 Array 而不是另一个 Set?【英文标题】:Why does Set#map return an Array rather than another Set?为什么 Set#map 返回一个 Array 而不是另一个 Set? 【发布时间】:2020-07-04 12:21:43 【问题描述】:刚刚遇到TypeError: no implicit conversion of Set into Array
做某种形式的事情
thingGetterSet.map|m| m.getThing .select|t| t.appropriate? + appropriateThingsSet
原来Set#map
返回一个数组,我希望一个集合映射到另一个集合。
即使它返回了一个集合,Set#select
也返回一个数组,我也期望一个集合。
这里发生了什么?为什么 Ruby 对我的规范无序的东西以及我想禁止重复的地方施加随机排序并允许重复?
【问题讨论】:
类Set
包含模块Enumerable
以使Enumerable
实例方法可供Set
的实例使用。 Enumerable#map 和 Enumerable#select 返回数组,而不管其接收者的类别(例如,数组、散列等)。
现在让您的生活更轻松:切换到snake case。
Ruby 不可能知道Set#map
的结果是一个集合。例如Set[1,2,3].map(&:odd?)
@spike map!
使接收器发生变异,因此它实际上返回一个集合是有意义的。 .map
根据定义迭代一个对象并返回一个数组。
set.dup.map!(&:odd?) == Set[true, false]
是一种非常有趣的方式来查看一组是否同时包含偶数和奇数。
【参考方案1】:
正如其中一位 cmets 所说,这里的问题是 Set 包含 Enumerable
作为包括 map
在内的一系列方法的提供者。这是一个语言设计决定,所以我能给出的最佳答案是将您指向开发人员的邮件列表。这是几年前的一个有趣的帖子:“Hash#select return type does not match Hash#find_all”,尤其是“[Ruby trunk Bug#13795] Hash#select return type does not match Hash#find_all”。
我在线程中看到了一些原因,没有重新实现map
和Set
中的其他人:
-
这与拥有通用 Enumerable 的好处背道而驰,导致包含该模块的类的额外实现工作,因为它们必须重新实现一堆方法(尽管这不是主要原因)。
map
的一些用途尚不清楚保留原始数据结构是个好主意。例如在 cmets 中使用:odd?
,在
.map |k, v| "#k-#v"
返回 Hash
是行不通的。
Range
和IO
,在这些类中保留它们的类型后映射没有意义,因此返回数组的默认行为更加一致。
【讨论】:
【参考方案2】:一般来说,有两种不同风格的集合操作库:
类型保留:这就是您对问题的期望 generic(不是“参数多态意义”,而是该词的标准英文意义)或者可能是“同质”类型保留集合操作尝试为 select
、take
、drop
等仅保留未修改的现有元素的操作保留类型。对于像map
这样的操作,它会尝试找到最接近的仍然可以保存结果的超类型。例如。将IntSet
映射到String
显然不会导致IntSet
,而只会导致Set
。将IntSet
映射到Boolean
可以在BitSet
中表示,但我知道没有足够聪明的集合框架能够真正做到这一点。
通用/同构集合操作总是返回相同的类型。通常,选择这种类型是非常通用的,以适应最广泛的用例。例如,在 .NET 中,集合操作返回 IEnumerable
,在 Java 中,它们返回 Stream
s,在 C++ 中,它们返回迭代器。
直到最近,只有通过复制所有类型的所有操作才能实现类型保留的集合操作。例如,Smalltalk 集合框架是类型保留的,它通过让每个集合类重新实现每个集合操作来实现这一点。这会导致大量重复代码,是维护的噩梦。 (许多新的面向对象抽象被发明出来的第一篇论文是关于如何将其应用于 Smalltalk 集合框架的,这绝非巧合。有关示例,请参见 Traits: Composable Units of Behaviour。)
据我所知,the Scala 2.8 re-design of the collections framework (see also this answer on SO) 是第一次有人设法创建保留类型的集合操作,同时最大限度地减少(尽管不是消除)重复。然而,the Scala 2.8 collections framework was widely criticized 过于复杂,并且在过去十年中需要不断的工作。事实上,它实际上也导致了对 Scala 文档系统的完全重新设计,只是为了能够hide the very complex type signatures that the type-preserving operations require。但是,this still wasn't enough,所以集合框架是completely thrown out and re-designed yet again in Scala 2.13。 (而这次重新设计花了几年时间。)
所以,“为什么 Ruby 集合框架不保留类型”的答案实际上很简单:因为 Ruby 是在 1993 年创建的,而我们(我的意思是整个编程社区)没有弄清楚如何正确地做到这一点,直到 26 年后的 2019 年。
还请注意,Scala 的实现严重依赖于静态类型。不仅是静态类型,还有编译时类型级编程、编译时类型级自省和编译时类型级元编程。这些不是必要的,但它们确实意味着你不能简单地将他们的解决方案复制到Ruby。例如,Scala 将使用 type classes 和 implicit search 来找出最佳匹配
IntSet(1, 2, 3).map(_.toString)
//=> val res: Set[String] = Set("1", "2", "3")
是一个Set[String]
在编译时。在 Ruby 中,您显然仍然可以运行相同的搜索算法,尽管它会慢得多,因为您需要在运行时运行它,每次运行 map
时都一遍又一遍地运行.它会更慢,但有可能:它只是一个算法,如果你可以在编译时运行它,那么你也可以在运行时运行它。但!该算法需要块的返回类型作为其参数之一!在 Scala 中,这是在编译时推断的。在 Ruby 中你是怎么知道的?
但即使在 Scala 中,有时也无法找到合适的匹配项,例如这里:
val m = Map(1 → "one", 2 → "two", 3 → "three")
m.map case (k, v) ⇒ s"$k $v"
//=> val res: Iterable[String] = List("1 one", "2 two", "3 three")
所以,Scala 可以找到的最好的静态类型是 Iterable
,它实际上是 Scala 集合层次结构的最顶层,Scala 可以找到的最好的运行时类型是 List
,它实际上是“go -to" Scala 中的集合类型,类似于 Ruby 中的 Array
。换句话说,这实际上是 Scala 在说“我放弃了,伙计。”
还有另一个问题,即类型保留的集合操作挑战了我们认为是某些操作合同的一部分。例如,大多数人会争辩说集合的基数在map
下应该是不变的,换句话说,map
应该将每个元素映射到一个新元素,因此map
不应该改变集合的大小.但是,这个带有保留类型的 Ruby 集合框架的假设代码怎么样:
Set[1, 2, 3].map(&:odd?)
#=> Set[true, false]
还有一些其他有趣的情况,我什至不知道在类型保留的集合框架中返回类型应该是什么,例如,Range
s 或 IO
流呢:
(1..1000).map(&:odd?)
(1..1000).select(&:odd?)
File.open('bla').map(&:upcase)
正因为如此,Ruby 的设计者选择在 Ruby 中使用同构集合操作:每个集合操作总是返回一个@ 987654360@.
嗯。好的。除了有时他们确实选择覆盖它们。例如。在Hash
中,过滤操作select
、reject
等实际上做 返回一个Hash
。但请注意,这是最近的变化,实际上有一段有趣的历史:
Hash#select
返回一个 Array
,但 Hash#reject
返回一个 Hash
!
在 Ruby 1.9 中,这已更改为 both return a Hash
。
但是,find_all
(在 Enumerable
中定义为 select
的别名)直到今天在 Hash
中没有被覆盖,因此它返回一个 @987654374 @!
另一方面,新引入的filter
,也定义为Enumerable
中select
的别名,在@中被覆盖987654378@ 返回Hash
。
因此,Ruby 设计者选择了简单性(没有代码重复,运行时没有复杂的类型计算,所有操作总是返回数组,所以没有像上面例子中的 map
改变集合的大小等意外。 ) 过度正确性,并使 Ruby 集合操作同质化而不是类型保留。但随后他们也选择了实用主义而不是纯粹性,并在各处散布了一些保留类型的覆盖。
所以,Set#map
返回一个Array
的事实应该不足为奇,因为集合框架中的每个其他类 也是这样做的。 only 更改 Set#map
不是一个好主意,IMO。 如果我们这样做,应该为map
的所有 实现者这样做。但这是一个重大的突破性变化,因此最早必须等到 Ruby 3。 (实际上,matz 说过他想避免在 Ruby 3 中进行重大更改。)但是即使为所有实现者仅更改map
也是很奇怪的,如果我们这样做, 所有操作都应该这样做。这是一项重大的研究任务,因此对于 Ruby 3 来说已经太晚了,所以至少要等到 Ruby 4。
然而,我们可以争论的是Array
是否是通用集合类型的正确选择。你可能会注意到其他类似的框架选择了一个非常通用的类型:.NET 有IEnumerable
,Java 有Stream
,C++ 有迭代器。 Ruby 中的等价物是Enumerator
。也许,Enumerator
应该是所有集合操作返回的类型。例如,如果你 map
超过一个无限集,结果将再次是无限的,但它将是一个 Array
,这意味着它需要无限量的内存!
不过,这让我们回到了实用主义:在大多数用例中,Array
比 Enumerator
更有用。
【讨论】:
以上是关于为啥 Set#map 返回一个 Array 而不是另一个 Set?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 document.querySelectorAll 返回的是 StaticNodeList 而不是真正的 Array?
为啥 array_pop() 返回数组的最后一项而不是删除它? [关闭]
为啥带有 Mysql2 Gem ActiveRecord::Base.connection.execute(sql) 的 Rails 3 返回 Array 而不是 Hash?
为啥返回带有箭头函数的对象时,array.map 中需要 return 关键字?