Scala集合如何能够从地图操作中返回正确的集合类型?

Kev*_*ght 60 collections scala

注意:这是一个常见问题解答,具体问我所以我可以自己回答,因为这个问题似乎经常出现,我想把它放在一个可以(希望)通过搜索很容易找到的地方

根据我在这里的回答评论提示


例如:

"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]
Run Code Online (Sandbox Code Playgroud)

查看scaladoc,所有这些都使用了map继承自的操作TraversableLike,那么为什么它始终能够返回最具体的有效集合呢?甚至String,它map通过隐式转换提供.

Kev*_*ght 79

Scala系列是聪明的东西......

收集库的内部是Scala土地上更高级的主题之一.它涉及更高级的类型,推理,方差,含义和CanBuildFrom机制 - 所有这些都使其从面向用户的角度来看具有令人难以置信的通用性,易用性和强大性.从API设计者的角度理解它并不是一个初学者要采取的轻松任务.

另一方面,你真的需要在这个深度上使用集合是非常罕见的.

那么让我们开始......

随着Scala 2.8的发布,集合库被完全重写以删除重复,许多方法被移动到一个地方,以便持续维护和添加新的收集方法将更容易,但它也使层次结构更难了解.

就拿List例如,这从继承(反过来)

  • LinearSeqOptimised
  • GenericTraversableTemplate
  • LinearSeq
  • Seq
  • SeqLike
  • Iterable
  • IterableLike
  • Traversable
  • TraversableLike
  • TraversableOnce

这是非常少数!为什么这个深层次的层次?XxxLike简单地忽略这些特征,该层次结构中的每个层都添加了一些功能,或者提供了更优化的继承功能版本(例如,通过索引获取元素Traversable需要组合drophead操作,对索引序列非常低效) ).在可能的情况下,所有功能都被推送到层次结构尽可能远的地方,最大化可以使用它的子类的数量并删除重复.

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]
Run Code Online (Sandbox Code Playgroud)

在每种情况下,输出尽可能与输入的类型相同.当它是不可能的,输入型的超检查直到一个发现确实提供了一个有效的返回类型.做到这一点需要做很多工作,特别是当你认为它String甚至不是一个集合时,它只能隐含地转换为一个集合.

那怎么办?

这个难题的一半是XxxLike特征(我确实说过我会得到它们......),其主要功能是采用Repr类型参数("表示"的缩写),以便它们知道真正的子类实际存在经营.因此,例如与类型参数TraversableLike相同Traversable,但是在Repr类型参数上进行抽象.然后在拼图的后半部分使用该参数; 所述CanBuildFrom捕获源集合型类型,目标元素类型和目标集合类型由收集-变换操作中.

通过一个例子来解释更容易!

BitSet定义了一个这样的隐式实例CanBuildFrom:

implicit def canBuildFrom: CanBuildFrom[BitSet, Int, BitSet] = bitsetCanBuildFrom
Run Code Online (Sandbox Code Playgroud)

编译时BitSet(1,2,3,4) map {2*},编译器将尝试隐式查找CanBuildFrom[BitSet, Int, T]

这是一个聪明的部分......范围中只有一个与前两个类型参数匹配的隐式.第一个参数是ReprXxxLike特征捕获的,第二个参数是元素类型,由当前集合特征(例如Traversable)捕获.map然后,该操作也使用类型进行参数化,该类型T是基于CanBuildFrom隐式定位的实例的第三类型参数来推断的.BitSet在这种情况下.

所以前两个类型参数CanBuildFrom是输入,用于隐式查找,第三个参数是输出,用于推理.

CanBuildFromBitSet因此匹配两种类型BitSetInt,所以查找会成功,并推断返回类型也将是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]
Run Code Online (Sandbox Code Playgroud)

它匹配,因为科尔是真实初始化是一种类型的别名BitSet时,BitSet从派生Set.在A将匹配任何东西,因为canBuildFrom是参数与类型A,在这种情况下,它推断是String......由此产生的返回类型Set[String].

因此,要正确实现集合类型,您不仅需要提供正确的类型隐式CanBuildFrom,还需要确保将该集合的具体类型作为Repr参数提供给正确的父特征(例如,将MapLike在子类化的情况下Map).

String因为它map通过隐式转换提供了一点点复杂.隐式转换是指最终派生的StringOps子类- 作为类型参数.StringLike[String]TraversableLike[Char,String]StringRepr

还有一个CanBuildFrom[String,Char,String]范围,以便编译器知道在将a的元素映射StringChars时,返回类型也应该是一个字符串.从这一点开始,使用相同的机制.

  • +1对Scala的类型保留集合转换的非常酷的解释.另外,StackOverflow的使用非常好.回到Ye Olde Podcasts,Jeff反复提到了这个用例(将洞察力作为一个问题,然后立即回答),而且我很少见到它. (3认同)
  • @Jörg然后更频繁地检查Scala问题.:-)我在某些场合自己使用它,我想我也看到别人也这样做了.我想我们scalazzi喜欢解释事情!:-) (2认同)

huy*_*hjl 8

Scala Collections在线页面的体系结构有一个详细的解释,面向基于2.8集合设计创建新集合的实际方面.

引用:

"如果你想集成一个新的集合类,那么需要做些什么,这样才能从正确类型的所有预定义操作中获益?在接下来的几页中,你将会看到两个这样做的例子."

它使用例如用于编码RNA序列的集合和用于Patricia trie的集合.查找" 处理地图和朋友"部分,了解如何返回相应的集合类型.