当静态类型为Map时,Scala SortedMap.map方法返回未排序的映射

时间:2012-09-27 20:08:26

标签: scala collections functional-programming higher-order-functions sortedmap

我在使用Scala的SortedMap [A​​,B]时遇到了一些未经授权的陌生感。如果我将SortedMap [A​​,B]“a”的引用声明为Map [A​​,B]类型,那么对“a”的映射操作将产生一个非有序映射实现。

示例:

import scala.collection.immutable._

object Test extends App {
    val a: Map[String, String] = SortedMap[String, String]("a" -> "s", "b" -> "t", "c" -> "u", "d" -> "v", "e" -> "w", "f" -> "x")
    println(a.getClass+": "+a)

    val b = a map {x => x}  // identity
    println(b.getClass+": "+b)
}

以上的输出是:

  

class scala.collection.immutable.TreeMap:Map(a - > s,b - > t,c - > u,d - > v,e - > w,f - > x)
  class scala.collection.immutable.HashMap $ HashTrieMap:Map(e - &gt; w,f - &gt; x,a - &gt; s,b - &gt; t,c - &gt; u,d - &gt; v)< / p>

身份转换之前和之后的键/值对的顺序不一样。

奇怪的是,从“a”中删除类型声明会使这个问题消失。这在玩具示例中很好,但是使得SortedMap [A​​,B]无法传递给期望Map [A​​,B]参数的方法。

一般来说,我希望更高阶的函数,如“map”和“filter”,不会改变它们应用的集合的基本属性。

有谁知道为什么“地图”表现得像这样?

4 个答案:

答案 0 :(得分:2)

map方法与大多数收集方法一样,并未专门针对SortedMap定义。它在更高级别的类(TraversableLike)上定义,并使用“构建器”将映射结果转换为正确的返回类型。

那么它如何决定“正确”的返回类型是什么?好吧,它会尝试返回它开始的返回类型。当你告诉Scala你有一个Map[String,String]并要求它map时,那么构建器必须弄清楚如何“构建”返回的类型。由于您告诉Scala输入为Map[String,String],因此构建器决定为您构建Map[String,String]。构建器不知道您想要SortedMap,因此它不会给您一个。

当您不使用Map[String,String]类型注释时,它的工作原理是Scala推断a的类型为SortedMap[String,String]。因此,当您调用map时,您在SortedMap上调用它,并且构建器知道构造SortedMap以便返回。

至于你断言方法不应该改变“基本属性”,我认为你是从错误的角度看待它。这些方法总是会返回一个符合您指定类型的对象。 type 定义了构建器的行为,而不是底层实现。当你想到这样的时候,就是 type 形成了方法应该如何表现的契约。

为什么我们想要这个?

为什么这是首选行为?让我们看一个具体的例子。假设我们有SortedMap[Int,String]

val sortedMap = SortedMap[Int, String](1 -> "s", 2 -> "t", 3 -> "u", 4 -> "v")

如果我使用修改键的函数来map,我冒着在键碰撞时丢失元素的风险:

scala> sortedMap.map { case (k, v) => (k / 2, v) }
res3: SortedMap[Int,String] = Map(0 -> s, 1 -> u, 2 -> v)
但是,嘿,那没关系。毕竟它是Map,我知道它是Map,所以我应该期待这种行为。

现在假设我们有一个接受Iterable对的函数:

def f(iterable: Iterable[(Int, String)]) = 
  iterable.map { case (k, v) => (k / 2, v) }

由于此函数与Map无关,如果此函数的结果的元素少于输入,则非常会令人惊讶。毕竟,map上的Iterable应该生成每个元素的映射版本。但Map Iterable对,因此我们可以将其传递给此函数。那么当我们这样做时,Scala会发生什么?

scala> f(sortedMap)
res4: Iterable[(Int, String)] = List((0,s), (1,t), (1,u), (2,v))

看那个!没有元素丢失!换句话说,Scala不会因违反我们对map的{​​{1}}应如何运作的期望而让我们感到惊讶。如果构建器尝试根据输入为Iterable的事实生成SortedMap,那么我们的函数SortedMap会产生令人惊讶的结果,这会很糟糕。

故事的寓意是:使用类型告诉集合框架如何处理您的数据。如果您希望代码能够预期地图已排序,则应将其键入f

答案 1 :(得分:1)

map的签名是:

def map[B, That](f: ((A, B)) ⇒ B)(implicit bf: CanBuildFrom[Map[A, B], B, That]): That

隐式参数bf用于构建结果集合。因此,在您的示例中,由于a的类型为Map[String, String],因此bf的类型为:

val cbf = implicitly[CanBuildFrom[Map[String, String], (String, String), Map[String, String]]]

只构建一个Map[String, String],它没有SortedMap的任何属性。参见:

cbf() ++= List("b" -> "c", "e" -> "g", "a" -> "b") result

有关详细信息,请参阅此优秀文章:http://docs.scala-lang.org/overviews/core/architecture-of-scala-collections.html

答案 2 :(得分:1)

正如dyross指出的那样,它是Builder,它是根据目标类型选择的(通过CanBuildFrom),它决定了你从map操作中获得的集合的类。现在这可能不是您想要的行为,但它确实允许您选择目标类型:

val b: SortedMap[String, String] = a.map(x => x)(collection.breakOut) 

breakOut给出一个通用CanBuildFrom,其类型由上下文确定,即我们的类型注释。)

因此,您可以添加一些类型参数,允许您接受任何类型的Map或Traversable(请参阅this question),这将允许您在保留正确的类型信息的同时在您的方法中执行映射操作,但是你可以看到它并不简单。

我认为更简单的方法是使用集合'mapflatMap等方法定义应用于集合的函数,而不是将集合本身​​发送到方法。 / p>

即。而不是

def f[Complex type parameters](xs: ...)(complex implicits) = ...
val result = f(xs)

val f: X => Y = ...
val results = xs map f

答案 3 :(得分:0)

简而言之:您明确声明a属于Map类型,Scala集合框架非常努力地使用mapfilter之类的高阶函数不改变它们应用的集合的基本属性,因此它也会返回Map,因为这是你明确告诉它你想要的。