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

标签 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

我尝试了 self 类型、抽象类型成员,但没有产生任何有用的结果。有什么线索吗?

最佳答案

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 ,并且仍然能够在生成的 map 上使用额外的方法。我通常会使用类而不是特征来执行此操作,因为可用的构造函数参数会导致更清晰的语法:

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"))

但它仍然不能很好地与集合框架配合。您从 map 开始,最终得到 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 type,它必须是 Traversable 的子类(注意这里使用 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)
}

到目前为止,一切都很好。但仍然有一个问题......它不适用于 map ,因为它们采用两个类型参数。解决方案是向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 <% TraversableLikeMoreFilterOperations构造函数?这是一个 View 绑定(bind),并允许可以隐式转换为 TraversableLike 的类型。以及直接子类。字符串就是一个典型的例子:

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

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

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

map 进去, map 出来。字符串进去,字符串出来。等等...

我还没有尝试过 Stream还没有,或者Set ,或 Vector ,但您可以确信,如果您这样做,它将返回与您开始时相同类型的集合。

关于scala - 在参数化类中混合通用特征而不重复类型参数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/5026055/

相关文章:

mysql - 为什么mysql在两个事务同时更新时不以可重复读模式锁定一行?

flutter - 在 dart 中将 int 转换为 bool

scala - 用于理解 scala 中的列表/集合

scala - 反向路由 - 使用多个路由文件 Play 2.2.x 应用程序

Scala 线程安全的 HashSet

implicit-conversion - Scala.js 隐式转换有时不起作用?

scala - 将第一个列表的元素分发到另一个列表/数组

scala - 如何检查scala中的返回值类型

scala - Spark SQL 执行笛卡尔连接而不是内连接

与字符串相等的 JavaScript 隐式转换