在参数化类中混合通用特征而不复制类型参数

时间:2011-02-17 07:11:08

标签: scala implicit-conversion scala-collections enrich-my-library

让我们假设我想创建一个可以混入任何Traversable [T]的特征。最后,我希望能够说出这样的话:

val m = Map("name" -> "foo") with MoreFilterOperations

并且在MoreFilterOperations上有方法,这些方法以Traversable提供的任何内容表示,例如:

def filterFirstTwo(f: (T) => Boolean) = filter(f) take 2

然而,问题显然是T未定义为MoreFilterOperations上的类型参数。一旦我这样做,它当然是可行的,但我的代码会读到:

val m = Map("name" -> "foo") with MoreFilterOperations[(String,String)]

或者如果我定义了这种类型的变量:

var m2: Map[String,String] with MoreFilterOperations[(String,String)] = ...

根据我的口味详细的方式。我希望以这样的方式定义特征,我可以将后者写成:

var m2: Map[String,String] with MoreFilterOperations

我尝试了自我类型,抽象类型成员,但它没有产生任何有用的东西。有线索吗?

3 个答案:

答案 0 :(得分:11)

Map("name" -> "foo")是函数调用而不是构造函数,这意味着你不能写:

Map("name" -> "foo") with MoreFilterOperations

你可以写的更多

val m = Map("name" -> "foo")
val m2 = m with MoreFilterOperations

要获得mixin,你必须使用具体的类型,天真的第一次尝试将是这样的:

def EnhMap[K,V](entries: (K,V)*) =
  new collection.immutable.HashMap[K,V] with MoreFilterOptions[(K,V)] ++ entries

此处使用工厂方法以避免复制类型参数。但是,这不起作用,因为++方法只会返回一个普通的HashMap,而不是mixin!

解决方案(正如Sam所建议的)是使用隐式转换来添加pimped方法。这将允许您使用所有常用技术转换Map,并且仍然可以在生成的地图上使用您的额外方法。我通常用类而不是特性来做这个,因为有可用的构造函数params会导致更清晰的语法:

class MoreFilterOperations[T](t: Traversable[T]) {
  def filterFirstTwo(f: (T) => Boolean) = t filter f take 2
}

object MoreFilterOperations {
  implicit def traversableToFilterOps[T](t:Traversable[T]) =
    new MoreFilterOperations(t)
}

这样你就可以写

val m = Map("name"->"foo", "name2"->"foo2", "name3"->"foo3")
val m2 = m filterFirstTwo (_._1.startsWith("n"))

但它仍然不能很好地与集合框架。您从地图开始,最后得到Traversable。这不是事情应该如何运作。这里的技巧是使用更高级的类型

来抽象集合类型
import collection.TraversableLike

class MoreFilterOperations[Repr <% TraversableLike[T,Repr], T] (xs: Repr) {
  def filterFirstTwo(f: (T) => Boolean) = xs filter f take 2
}

足够简单。您必须提供表示集合的类型Repr和元素类型T。我使用TraversableLike代替Traversable,因为它嵌入了其表示形式;如果没有这个,filterFirstTwo将返回Traversable,无论起始类型如何。

现在是隐式转换。这就是类型符号中的事情变得有点棘手的地方。首先,我使用更高级的类型来捕获集合的表示:CC[X] <: Traversable[X],这将参数化CC类型,它必须是Traversable的子类(注意使用{{1在这里作为占位符,X并不意味着相同的事情。)

还有一个隐式CC[_] <: Traversable[_],编译器使用它来静态地保证我们的集合CC[T] <:< TraversableLike[T,CC[T]]真正是CC[T]的子类,因此TraversableLike的有效参数构造:

MoreFilterOperations

到目前为止,这么好。但是还有一个问题......它不适用于地图,因为它们有两个类型参数。解决方案是使用与以前相同的原则向object MoreFilterOperations { implicit def traversableToFilterOps[CC[X] <: Traversable[X], T] (xs: CC[T])(implicit witness: CC[T] <:< TraversableLike[T,CC[T]]) = new MoreFilterOperations[CC[T], T](xs) } 对象添加另一个隐式:

MoreFilterOperations

当你还想要使用不是实际集合的类型,但可以被视为好像它们时,真正的美丽就会出现。还记得implicit def mapToFilterOps[CC[KX,VX] <: Map[KX,VX], K, V] (xs: CC[K,V])(implicit witness: CC[K,V] <:< TraversableLike[(K,V),CC[K,V]]) = new MoreFilterOperations[CC[K,V],(K,V)](xs) 构造函数中的Repr <% TraversableLike吗?这是一个视图绑定,并允许可以隐式转换为MoreFilterOperations的类型以及直接子类。字符串就是一个典型的例子:

TraversableLike

如果您现在在REPL上运行它:

implicit def stringToFilterOps
(xs: String)(implicit witness: String <%< TraversableLike[Char,String])
: MoreFilterOperations[String, Char] =
  new MoreFilterOperations[String, Char](xs)

地图进入,地图出来了。字符串进入,字符串出来。等...

我还没有使用val m = Map("name"->"foo", "name2"->"foo2", "name3"->"foo3") // m: scala.collection.immutable.Map[java.lang.String,java.lang.String] = // Map((name,foo), (name2,foo2), (name3,foo3)) val m2 = m filterFirstTwo (_._1.startsWith("n")) // m2: scala.collection.immutable.Map[java.lang.String,java.lang.String] = // Map((name,foo), (name2,foo2)) "qaxfwcyebovjnbointofm" filterFirstTwo (_ < 'g') //res5: String = af StreamSet进行尝试,但您可以确信,如果您这样做,它将返回相同的类型您开始使用的集合。

答案 1 :(得分:2)

这不是你要求的,但你可以用implicits来解决这个问题:

trait MoreFilterOperations[T] {
  def filterFirstTwo(f: (T) => Boolean) = traversable.filter(f) take 2
  def traversable:Traversable[T]
}

object FilterImplicits {
  implicit def traversableToFilterOps[T](t:Traversable[T]) = new MoreFilterOperations[T] { val traversable = t }
}

object test {

  import FilterImplicits._

  val m = Map("name" -> "foo", "name2" -> "foo2", "name3" -> "foo3")
  val r = m.filterFirstTwo(_._1.startsWith("n"))
}

scala> test.r
res2: Traversable[(java.lang.String, java.lang.String)] = Map((name,foo), (name2,foo2))

答案 2 :(得分:1)

Scala标准库为此目的使用了implicits。例如。 "123".toInt。在这种情况下,我认为这是最好的方式。

否则,您将不得不完全实施“带有其他操作的地图”,因为不可变集合需要创建新混合类的新实例。

使用可变集合,您可以执行以下操作:

object FooBar {
  trait MoreFilterOperations[T] {
    this: Traversable[T] =>
    def filterFirstTwo(f: (T) => Boolean) = filter(f) take 2
  }

  object moreFilterOperations {
    def ~:[K, V](m: Map[K, V]) = new collection.mutable.HashMap[K, V] with MoreFilterOperations[(K, V)] {
      this ++= m
    }
  }

  def main(args: Array[String]) {
    val m = Map("a" -> 1, "b" -> 2, "c" -> 3) ~: moreFilterOperations
    println(m.filterFirstTwo(_ => true))
  }
}

我宁愿使用暗示。