7

我在使用 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 -> w, f -> x, a -> s, b -> t, c -> u, d -> v)

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

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

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

有谁知道为什么“地图”会这样?

4

4 回答 4

2

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

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

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

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

为什么我们想要这个?

为什么这是首选行为?让我们看一个具体的例子。假设我们有一个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毕竟是 a ,而且我知道它是 a Map,所以我应该期待这种行为。

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

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

由于这个函数与 s 无关,如果这个函数的结果的元素比输入的少Map,那将是非常令人惊讶的。毕竟,mapon aIterable应该产生每个元素的映射版本。但是 aMap Iterable对中的一个,所以我们可以将它传递给这个函数。那么当我们这样做时,Scala 会发生什么?

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

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

所以这个故事的寓意是:使用类型告诉集合框架如何处理您的数据。如果您希望您的代码能够预期地图已排序,那么您应该将其键入为SortedMap.

于 2012-09-27T20:20:29.947 回答
1

的签名map是:

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

隐式参数bf用于构建结果集合。因此,在您的示例中,由于ais的类型,因此 ofMap[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

于 2012-09-27T21:39:34.663 回答
1

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

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

breakOut给出一个CanBuildFrom类型由上下文决定的泛型,即我们的类型注解。)

因此,您可以添加一些类型参数,允许您接受任何类型的 Map 或 Traversable(请参阅此问题),这将允许您在方法中执行映射操作,同时保留正确的类型信息,但正如您所见,这并不简单.

我认为一个更简单的方法是使用 collections' 等方法来定义应用于集合的函数mapflatMap而不是通过将集合本身​​发送到方法。

即代替

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

val f: X => Y = ...
val results = xs map f
于 2012-09-27T22:53:07.193 回答
0

简而言之:您明确声明a为 type Map,并且 Scala 集合框架非常努力地尝试高阶函数,例如map并且filter不更改它们所应用的集合的基本属性,因此它也会返回 aMap因为这就是你明确告诉它你想要。

于 2012-09-27T23:46:38.287 回答