注意到我的代码实际上是在迭代列表并更新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)))
}
此代码无法编译,因为self updated (k, f(self(k)))
isa scala.collection.Map[A, B]
,而不是MapType[A, B]
。换句话说,self.updated
的返回类型就好像self
的类型是上层类型而不是实际声明的类型。
我可以&#34;修复&#34;转发的代码:
def updatedWith(k: A, f: B => B): MapType[A, B] =
self.updated(k, f(self(k))).asInstanceOf[MapType[A, B]]
这种感觉并不令人满意,因为向下转换是一种代码气味并且表明对类型系统的误用。在这个特殊的情况下,似乎值总是为cast-to类型,整个程序编译并正确运行,这个downcast支持这个视图,但它仍然闻起来。
那么,是否有更好的方法来编写此代码以使scalac在不使用向下转换的情况下正确推断类型,或者这是编译器限制并且需要向下转换?
[编辑添加以下内容。]
我的代码使用这种方法有点复杂和混乱,因为我仍在探索一些想法,但一个例子最小的例子是计算频率分布作为一个方面 - 代码的效果大致如下:
var counts = Map.empty[Int, Int] withDefaultValue 0
for (item <- items) {
// loads of other gnarly item-processing code
counts = counts updatedWith (count, 1 + _)
}
在撰写本文时,我的问题有三个答案。
归结为只让updatedWith
返回scala.collection.Map[A, B]
。从本质上讲,我的原始版本接受并返回immutable.Map[A, B]
,并使类型不那么具体。换句话说,它仍然不够通用,并设置调用者使用哪种类型的策略。我当然可以更改counts
声明中的类型,但这也是一个代码气味,可以解决返回错误类型的库,而它真正做的就是将向下转换为调用者的代码。所以我根本不喜欢这个答案。
另外两个是CanBuildFrom
和构建器的变体,因为它们基本上遍历地图以生成修改后的副本。一个内联修改后的updated
方法,而另一个方法调用原始updated
并将其附加到构建器,因此似乎可以创建一个额外的临时副本。两者都是解决类型正确性问题的好答案,尽管从性能角度来看,避免额外复制的问题更好,我更喜欢这个问题。然而另一个更短,可以说更清楚地表明意图。
如果假设的不可变映射以与List类似的方式共享大型树,则此复制会破坏共享并降低性能,因此最好使用现有的modified
而不执行复制。然而,Scala的不可变地图似乎并没有这样做,因此复制(一次)似乎是一种实用的解决方案,不太可能在实践中产生任何影响。
答案 0 :(得分: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]
实现:
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)
答案 1 :(得分:2)
请注意updated
方法返回Map
类,而不是泛型,所以我想你也可以返回Map
。但是如果你真的想要返回一个合适的类型,你可以看一下updated
中List.updated
的实现
我写了一个小例子。我不确定它是否涵盖了所有情况,但它适用于我的测试。我也使用了mutable Map
,因为我很难测试不可变的,但我想它可以很容易地转换。
implicit class RichMap[A, B, MapType[x, y] <: Map[x, y]](val self: MapType[A, B]) extends AnyVal {
import scala.collection.generic.CanBuildFrom
def updatedWith[R >: B](k: A, f: B => R)(implicit bf: CanBuildFrom[MapType[A, B], (A, R), MapType[A, R]]): MapType[A, R] = {
val b = bf(self)
for ((key, value) <- self) {
if (key != k) {
b += (key -> value)
} else {
b += (key -> f(value))
}
}
b.result()
}
}
import scala.collection.immutable.{TreeMap, HashMap}
val map1 = HashMap(1 -> "s", 2 -> "d").updatedWith(2, _.toUpperCase()) // map1 type is HashMap[Int, String]
val map2 = TreeMap(1 -> "s", 2 -> "d").updatedWith(2, _.toUpperCase()) // map2 type is TreeMap[Int, String]
val map3 = HashMap(1 -> "s", 2 -> "d").updatedWith(2, _.asInstanceOf[Any]) // map3 type is HashMap[Int, Any]
请注意CanBuildFrom
模式更强大,此示例并未使用它的所有功能。感谢CanBuildFrom
,某些操作可以完全更改集合的类型,例如BitSet(1, 3, 5, 7) map {_.toString }
类型实际上是SortedSet[String]
。