Scala: 为什么mapValues会产生一个视图,是否有稳定的替代方案?

63

刚刚我惊讶地发现 mapValues 会产生一个视图。这个后果在以下示例中展现:

case class thing(id: Int)
val rand = new java.util.Random
val distribution = Map(thing(0) -> 0.5, thing(1) -> 0.5)
val perturbed = distribution mapValues { _ + 0.1 * rand.nextGaussian }
val sumProbs = perturbed.map{_._2}.sum
val newDistribution = perturbed mapValues { _ / sumProbs }

我的想法是,我有一个分布,会受到一些随机扰动,然后我会重新归一化它。实际上,代码在原本的意图上失败了:因为mapValues生成了一个view,所以无论何时使用perturbed_ + 0.1 * rand.nextGaussian 都将被重新评估。

现在我正在做类似于distribution map { case (s, p) => (s, p + 0.1 * rand.nextGaussian) }这样的事情,但那只是有点冗长。因此,这个问题的目的是:

  1. 提醒那些不知道这个事实的人。
  2. 寻找使mapValues输出view的原因。
  3. 是否有其他方法可以产生具体的Map
  4. 是否还有其他常用的集合方法会遇到这种陷阱。

谢谢。


2
天啊,我又在另一个地方使用了 mapValues,这就是为什么有时候运行得如此缓慢的原因... - Kane
1
我也被这个问题困扰过。 - ziggystar
今天我也陷入了这个陷阱。 - Stanislav Savulchik
今天刚被这个问题咬了一口! - xysun
我知道这种行为,但我仍然会陷入这个陷阱,更糟糕的是,调试起来非常痛苦。 - Reinstate Monica
我刚刚被这个问题咬了一口,因为我在mapValues中创建了可变对象,然后困惑它们为什么没有更新。 - Karol S
3个回答

41

这个问题有一个关于它的工单,SI-4776 (由YT提交)。

引入此问题的提交有以下内容:

根据jurdolph的建议,使filterKeysmapValues转换为抽象映射,并复制了不可变映射的功能。将transformfilterNot从不可变映射移动到通用映射。经phaller审核。

我没有找到jrudolph的原始建议,但我认为这是为了使mapValues更有效率。鉴于这个问题,这可能会让人惊讶,但是如果您不太可能对值进行多次迭代,则mapValues更有效率。

作为解决方法,可以执行mapValues(...).view.force来生成新的Map


5
好的,但我想知道为什么 mapValues 不直接返回视图,以使其更加明确? - Alois Cochard
1
@AloisCochard 是的,那是个好观点。如果返回类型是 view,那么我们就能知道发生了什么... - Kane
3
如您在工单中所见,这正是我所询问的内容。这样做还有进一步的好处,可以直接使用“force”方法。 - Daniel C. Sobral
我不明白这个决定背后的理由。仅当现有键的调用次数少于条目数时,使用视图才能提高性能;根据我的经验,在大多数情况下,这种情况很少发生。如果底层数据是可变的(请参见SQL),则视图具有其他优点,但在Scala中这不太可取,因为它违反了函数式方法。 - Eyal Roth

11

Scala文档中写道:

  

一个映射视图,将该映射的每个key映射到f(this(key))。生成的映射包装原始映射而不复制任何元素。

因此,这应该是可以预期的,但这让我非常害怕,我明天将不得不审核一堆代码。我没有预料到会出现这样的行为:-(

另一种解决方法:

您可以调用toSeq以获得副本,并且如果您需要将其返回到地图toMap,但是这会不必要地创建对象,并且与使用map相比具有性能影响

人们可以相对容易地编写一个mapValues,它不会创建视图,如果没有人在我之前实现它,我明天会在此处发布代码;)

编辑:

我发现了一个'强制'视图的简单方法,在mapValues后使用'.map(identity)'即可(因此无需实现特定函数):

scala> val xs = Map("a" -> 1, "b" -> 2)
xs: scala.collection.immutable.Map[java.lang.String,Int] = Map(a -> 1, b -> 2)

scala> val ys = xs.mapValues(_ + Random.nextInt).map(identity)
ys: scala.collection.immutable.Map[java.lang.String,Int] = Map(a -> 1315230132, b -> 1614948101)

scala> ys
res7: scala.collection.immutable.Map[java.lang.String,Int] = Map(a -> 1315230132, b -> 1614948101)

真遗憾,返回的类型实际上并不是视图!否则就可以调用'force'了...


1
运行Scala 2.12.0-M3,我不清楚 map(identity) 给你带来了什么:Map("a" -> 1, "b" -> 2).mapValues(_ + Random.nextInt) 返回 scala.collection.immutable.Map[String,Int] = Map(a -> 1496073565, b -> -1842623900)。你能详细说明一下吗?我认为 mapValues 的潜在问题是它会惰性地评估值,但我不确定。谢谢。 - Kevin Meredith
2
这太令人惊讶了。View 的 map 怎么会返回一个具体的 Map 而不是 View?这已经超出了不一致性,完全是缺乏考虑。从源代码确认这个问题在 2.12 版本中仍然存在。 - Jason Hu

1
在Scala 2.13中,更好(已弃用),现在返回一个MapView:API文档

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接