避免泛型类中的向下倾斜

pnd*_*ndc 3 scala

注意到我的代码基本上迭代了列表并更新了Map中的值,我首先创建了一个简单的辅助方法,该方法采用了一个函数来转换map值并返回一个更新的map.随着程序的发展,它获得了一些其他Map转换函数,因此很自然地将它转换为一个隐式值类,可以添加方法scala.collection.immutable.Map[A, B].该版本运行正常.

但是,没有任何关于需要特定地图实现的方法,它们似乎适用于scala.collection.Map[A, B]甚至是MapLike.所以我希望它在地图类型以及键和值类型中是通用的.这就是梨形的地方.

我当前的迭代看起来像这样:

implicit class RichMap[A, B, MapType[A, B] <: collection.Map[A, B]](
    val self: MapType[A, B]
) extends AnyVal {
  def updatedWith(k: A, f: B => B): MapType[A, B] =
    self updated (k, f(self(k)))
}
Run Code Online (Sandbox Code Playgroud)

此代码无法编译,因为self updated (k, f(self(k)))isa scala.collection.Map[A, B],它不是MapType[A, B].换句话说,返回类型self.updated就好像self's类型是上层类型绑定而不是实际声明类型.

我可以通过向下转换"修复"代码:

def updatedWith(k: A, f: B => B): MapType[A, B] =
  self.updated(k, f(self(k))).asInstanceOf[MapType[A, B]]
Run Code Online (Sandbox Code Playgroud)

这种感觉并不令人满意,因为向下转换是一种代码气味并且表明对类型系统的误用.在这种特殊情况下,似乎值始终是强制转换类型,并且整个程序编译并正确运行此向下转换支持此视图,但它仍然有气味.

那么,是否有更好的方法来编写此代码以使scalac在不使用向下转换的情况下正确推断类型,或者这是编译器限制并且需要向下转换?

[已编辑添加以下内容.]

使用这种方法的代码有点复杂和混乱,因为我仍在探索一些想法,但最小的例子是将频率分布计算为副作用,代码大致如下:

var counts = Map.empty[Int, Int] withDefaultValue 0
for (item <- items) {
  // loads of other gnarly item-processing code
  counts = counts updatedWith (count, 1 + _)
}
Run Code Online (Sandbox Code Playgroud)

在撰写本文时,我的问题有三个答案.

无论如何,其中一个归结为让updatedWith回归scala.collection.Map[A, B].从本质上讲,它接受并返回了我的原始版本,immutable.Map[A, B]并使类型不那么具体.换句话说,它仍然不够通用,并为调用者使用的类型设置策略.我当然可以更改counts声明中的类型,但这也是一个代码气味来解决返回错误类型的库,而它真正做的就是将向下转换为调用者的代码.所以我根本不喜欢这个答案.

另外两个是变体CanBuildFrom和构建器,因为它们基本上遍历地图以产生修改后的副本.一个内联修改后的updated方法,而另一个调用原始内容updated并将其附加到构建器,因此似乎可以创建一个额外的临时副本.两者都是解决类型正确性问题的好答案,尽管从性能角度来看,避免额外复制的问题更好,我更喜欢这个问题.然而另一个更短,可以说更清楚地表明意图.

如果假设的不可变映射以与List类似的方式共享大型树,则此复制将破坏共享并降低性能,因此最好使用现有modified而不执行复制.但是,Scala的不可变映射似乎并没有这样做,因此复制(一次)似乎是一种实用的解决方案,在实践中不太可能产生任何影响.

Mic*_*jac 5

是! 使用CanBuildFrom.这就是Scala集合库如何使用CanBuildFrom证据推断出最接近的集合类型.只要您有隐含的证据CanBuildFrom[From, Elem, To],From您开始使用的集合类型在哪里,就是集合中Elem包含的类型,并且To是您想要的最终结果.该CanBuildFrom会提供一个Builder可向其中添加元素,当你做,你可以打电话Builder#result()来获得适当类型的已完成收集.

在这种情况下:

From = MapType[A, B]
Elem = (A, B) // The type actually contained in maps
To = MapType[A, B]
Run Code Online (Sandbox Code Playgroud)

执行:

import scala.collection.generic.CanBuildFrom

implicit class RichMap[A, B, MapType[A, B] <: collection.Map[A, B]](
    val self: MapType[A, B]
) extends AnyVal {
  def updatedWith(k: A, f: B => B)(implicit cbf: CanBuildFrom[MapType[A, B], (A, B), MapType[A, B]]): MapType[A, B] = {
    val builder = cbf()
    builder ++= self.updated(k, f(self(k)))
    builder.result()
  }
}

scala> val m = collection.concurrent.TrieMap(1 -> 2, 5 -> 3)
m: scala.collection.concurrent.TrieMap[Int,Int] = TrieMap(1 -> 2, 5 -> 3)

scala> m.updatedWith(1, _ + 10)
res1: scala.collection.concurrent.TrieMap[Int,Int] = TrieMap(1 -> 12, 5 -> 3)
Run Code Online (Sandbox Code Playgroud)