Scala 集合如何从映射操作中返回正确的集合类型?
Posted
技术标签:
【中文标题】Scala 集合如何从映射操作中返回正确的集合类型?【英文标题】:How are Scala collections able to return the correct collection type from a map operation? 【发布时间】:2011-07-09 04:49:43 【问题描述】:注意:这是一个常见问题解答,专门询问,所以我可以自己回答,因为这个问题似乎经常出现,我想把它放在一个可以(希望)通过以下方式轻松找到的位置搜索
根据我的 answer here 上的评论提示
例如:
"abcde" map _.toUpperCase //returns a String
"abcde" map _.toInt // returns an IndexedSeq[Int]
BitSet(1,2,3,4) map 2* // returns a BitSet
BitSet(1,2,3,4) map _.toString // returns a Set[String]
查看scaladoc,所有这些都使用从TraversableLike
继承的map
操作,那么为什么它总是能够返回最具体的有效集合?甚至是String
,它通过隐式转换提供map
。
【问题讨论】:
Scala 2.8 collections design tutorial的可能重复 一开始跳过了斜体部分,一时间陷入了深深的困惑。詹姆斯,链接的帖子中并没有真正回答这个具体问题。只是说隐含与它有关。假设凯文想要更精细,这不是重复。 @James - 这有点不同,我更专注于CanBuildFrom
机制
将其称为“能够返回最合适的集合”是相当牵强的。
@Tony:如果我们允许“适当”的定义与允许实体移动的上下文相关,那么该陈述将变得准确。 鉴于对集合的操作结果将是另一个集合,可以公平地说,如果可能的话,最“合适”的集合可能是另一个相同类型的集合,并且最具体的替代方案。不过,Logan 是对的,这应该是评论,而不是答案。
【参考方案1】:
Scala 集合是聪明的东西...
集合库的内部是 Scala 领域中更高级的主题之一。它涉及到更高级的类型、推理、方差、隐式和CanBuildFrom
机制——所有这些都是为了使其非常通用、易于使用并且从面向用户的角度来看功能强大。从 API 设计者的角度来理解它,对于初学者来说并不是一项轻松的任务。
另一方面,您实际上需要在这种深度使用集合是非常罕见的。
那么让我们开始吧……
随着 Scala 2.8 的发布,集合库被完全重写以消除重复,大量的方法被移动到一个地方,这样持续的维护和添加新的集合方法会容易得多,但这也使得层次结构更难理解。
以List
为例,这个继承自(依次)
LinearSeqOptimised
GenericTraversableTemplate
LinearSeq
Seq
SeqLike
Iterable
IterableLike
Traversable
TraversableLike
TraversableOnce
这是相当少数!那么为什么会有这么深的层次结构呢?暂时忽略XxxLike
特征,该层次结构中的每一层都会添加一些功能,或者提供更优化的继承功能版本(例如,通过Traversable
上的索引获取元素需要drop
的组合和head
操作,在索引序列上效率极低)。在可能的情况下,所有功能都尽可能地向上推到层次结构中,最大限度地增加可以使用它的子类的数量并消除重复。
map
就是这样一个例子。该方法在TraversableLike
中实现(尽管XxxLike
特征只存在于库设计者中,因此对于大多数意图和目的来说,它通常被认为是Traversable
上的一种方法——我很快就会谈到那部分),并且被广泛继承。可以在某些子类中定义优化版本,但它仍必须符合相同的签名。考虑map
的以下用法(问题中也提到过):
"abcde" map _.toUpperCase //returns a String
"abcde" map _.toInt // returns an IndexedSeq[Int]
BitSet(1,2,3,4) map 2* // returns a BitSet
BitSet(1,2,3,4) map _.toString // returns a Set[String]
在每种情况下,输出都尽可能与输入类型相同。如果不可能,则检查输入类型的超类,直到发现 确实 提供了有效的返回类型。要做到这一点需要做很多工作,尤其是当您考虑到 String
甚至不是一个集合,它只是隐式转换为一个。
那么它是怎么做的呢?
难题的一半是XxxLike
特征(我确实说我会找到它们...),其主要功能是采用Repr
类型参数(简称对于“表示”),以便他们知道实际操作的真正子类。所以例如TraversableLike
与 Traversable
相同,但在 Repr
类型参数上进行了抽象。然后这个参数被拼图的后半部分使用;捕获源集合类型、目标元素类型和目标集合类型的CanBuildFrom
类型类,以供集合转换操作使用。
举个例子更容易解释!
BitSet 定义了一个 CanBuildFrom
的隐式实例,如下所示:
implicit def canBuildFrom: CanBuildFrom[BitSet, Int, BitSet] = bitsetCanBuildFrom
编译BitSet(1,2,3,4) map 2*
时,编译器会尝试隐式查找CanBuildFrom[BitSet, Int, T]
这是聪明的部分...只有一个隐含的范围与前两个类型参数匹配。第一个参数是Repr
,由XxxLike
特征捕获,第二个参数是元素类型,由当前集合特征捕获(例如Traversable
)。然后map
操作也使用类型参数化,此类型T
是根据隐式定位的CanBuildFrom
实例的第三个类型参数推断的。 BitSet
在这种情况下。
所以CanBuildFrom
的前两个类型参数是输入,用于隐式查找,第三个参数是输出,用于推理。
BitSet
中的CanBuildFrom
因此匹配BitSet
和Int
这两种类型,所以查找会成功,推断的返回类型也会是BitSet
。
编译BitSet(1,2,3,4) map _.toString
时,编译器将尝试隐式查找CanBuildFrom[BitSet, String, T]
。对于 BitSet 中的隐式,这将失败,因此编译器接下来将尝试其超类 - Set
- 这包含隐式:
implicit def canBuildFrom[A]: CanBuildFrom[Coll, A, Set[A]] = setCanBuildFrom[A]
匹配,因为 Coll 是一个类型别名,当 BitSet
派生自 Set
时,它被初始化为 BitSet
。 A
将匹配任何内容,因为canBuildFrom
使用A
类型参数化,在这种情况下,它被推断为String
...因此产生Set[String]
的返回类型。
所以要正确实现一个集合类型,你不仅需要提供一个正确的隐式类型CanBuildFrom
,还需要确保该集合的具体类型作为Repr
参数提供给正确的父特征(例如,在子类化Map
的情况下,这将是MapLike
)。
String
稍微复杂一点,因为它通过隐式转换提供map
。隐式转换为StringOps
,它是StringLike[String]
的子类,最终派生TraversableLike[Char,String]
- String
是Repr
类型参数。
在范围内还有一个CanBuildFrom[String,Char,String]
,以便编译器知道当将String
的元素映射到Char
s 时,返回类型也应该是一个字符串。从现在开始,使用相同的机制。
【讨论】:
+1 非常酷地解释了 Scala 的类型保留集合转换。此外,*** 的使用非常好。回到 Ye Olde Podcasts,Jeff 反复提到这个用例(将洞察力表达为一个问题,然后立即回答它),而我很少看到它。 @Jörg 然后更频繁地检查 Scala 问题。 :-) 我在某些情况下自己使用过它,我想我也看到其他人也这样做过。我想我们scalazzi喜欢解释事情! :-) 我一直在思考为什么尝试将breakOut
和Iterator
转换为List
无法编译。在阅读了这篇文章和@DanielC.Sobral answer here 之后,我终于意识到a)我确实了解这一切是如何工作的,b)Iterator.map(...)
没有在第二个参数列表中使用隐式 CanBuildFrom ......为什么会这样? 【参考方案2】:
Architecture of Scala Collections 在线页面对基于 2.8 集合设计创建新集合的实际方面进行了详细说明。
引用:
“如果你想集成一个新的集合类,需要做些什么,以便它可以从正确类型的所有预定义操作中受益?在接下来的几页中,你将通过两个示例来完成此操作。 "
它使用一个用于编码 RNA 序列的集合和一个用于 Patricia trie 的集合作为示例。查找处理地图和朋友部分,了解如何返回适当的集合类型。
【讨论】:
以上是关于Scala 集合如何从映射操作中返回正确的集合类型?的主要内容,如果未能解决你的问题,请参考以下文章