避免泛型类中的向下倾斜

时间:2016-01-25 14:33:22

标签: 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)))
}

此代码无法编译,因为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的不可变地图似乎并没有这样做,因此复制(一次)似乎是一种实用的解决方案,不太可能在实践中产生任何影响。

2 个答案:

答案 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。但是如果你真的想要返回一个合适的类型,你可以看一下updatedList.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]