在通用方法中返回原始集合类型

19

假设我们想要创建一个类似于minBy的函数,它返回集合中所有相等最小值的元素:

def multiMinBy[A, B: Ordering](xs: Traversable[A])(f: A => B) = {
  val minVal = f(xs minBy f)
  xs filter (f(_) == minVal)
}

scala> multiMinBy(List("zza","zzza","zzb","zzzb"))(_.last)
res33: Traversable[java.lang.String] = List(zza, zzza)

到目前为止,一切都很好,除了我们得到的是一个Traversable而不是我们最初的List

于是我尝试将签名更改为

def multiMinBy[A, B: Ordering, C <: Traversable[A]](xs: C)(f: A => B)

我希望得到一个C,而不是Traversable[A],但是我没有得到任何返回值:

scala> multiMinBy(List("zza","zzza","zzb","zzzb"))(_.last)

<console>:9: error: inferred type arguments [Nothing,Nothing,List[java.lang.String]] 
do not conform to method multiMinBy's type parameter bounds [A,B,C <: Traversable[A]]

我认为这是因为在推断出 A 之前,C 出现在参数中了。因此,我颠倒了参数的顺序,并添加了一个强制转换:

def multiMinBy[A, B: Ordering, C <: Traversable[A]](f: A => B)(xs: C) = {
  val minVal = f(xs minBy f)
  (xs filter (f(_) == minVal)).asInstanceOf[C]
}

它是有效的,除了我们必须这样调用它:

multiMinBy((x: String) => x.last)(List("zza","zzza","zzb","zzzb"))

有没有一种方法可以保留原始语法,同时获取正确的集合类型返回?

3个回答

23

我认为Miles Sabin的解决方案过于复杂了。Scala的集合已经具备实现所需的基础设施,只需要进行非常小的更改即可:

import scala.collection.TraversableLike
def multiMinBy[A, B: Ordering, C <: Traversable[A]]
              (xs: C with TraversableLike[A, C])
              (f: A => B): C = {
  val minVal = f(xs minBy f)
  xs filter (f(_) == minVal)
}

3
我同意,这比我的方案更好。 - Miles Sabin
你可以解释一下为什么要使用GreaterLowerBound: TraversableLike[A, C]吗?你是怎么知道需要这个才能使方法返回C的?我不太明白输入的具体类型保存在哪里。 - Adrian
1
@Adrian,使用Scala 2.13,这可能已经不再是真实的情况了。事实上,我怀疑它是否正确。Traversable[A]约束提供了仅依赖于项目类型A的方法。TraversableLike[A, C]则提供了不仅处理项目类型为A的方法,而且还会返回类型C的方法。因为它是C with TraversableLike[A,C],所以我可以在其上调用一个方法,并将返回相同的集合C,而不是返回Traversable[A]filter方法就是这种情况,如果没有此限制,它将不会返回C - Daniel C. Sobral
啊,这个 with 关键字结合了 Traversable[A] 和 TraversableLike[A, C] 中的方法。 - Adrian
1
@Adrian,不是方法的联合,而是类型的统一。两者都存在相同的方法(filter),但是TraversableLike上的返回类型更具体。我本可以只使用TraversableLike,但我猜测——我真的不记得了——如果我这样做,类型推断就无法工作。 - Daniel C. Sobral

13

使用 CanBuildFrom 怎么样?

import scala.collection.immutable._
import scala.collection.generic._

def multiMinBy[A, B, From[X] <: Traversable[X], To](xs: From[A])(f: A => B)
  (implicit ord: Ordering[B], bf: CanBuildFrom[From[_], A, To])  = {
  val minVal = f(xs minBy f)
  val b = bf()
  b ++= (xs filter (f(_) == minVal))
  b.result
} 



scala> multiMinBy(List("zza","zzza","zzb","zzzb"))(_.last)
res1: List[java.lang.String] = List(zza, zzza)

10
你的问题是,当将其视为GenTraversable[A]方法时(在本答案中,我会使用它来代替Traversable[A]),filter方法的结果类型并不比GenTraversable[A]更精确。不幸的是,在当前multiMinBy方法的代码块中,这就是你所知道的有关xs的全部信息。
为了得到你想要的结果,你需要让multiMinBy的签名更加精确。一种方法是仍然保留相对开放的容器类型,同时使用结构类型,如下所示:
type HomFilter[CC[X] <: GenTraversable[X], A] = 
  CC[A] { def filter(p : A => Boolean) : CC[A] }

def multiMinBy[CC[X] <: GenTraversable[X], A, B: Ordering]
  (xs: HomFilter[CC, A])(f: A => B) : CC[A] = {
    val minVal = f(xs minBy f)
    xs filter (f(_) == minVal)
  }

结构类型HomFilter 允许我们断言传递给 multiMinBy 的参数必须具有所需结果类型的 filter 方法。

示例REPL会话,

scala> val mmb = multiMinBy(List("zza","zzza","zzb","zzzb"))(_.last)
mmb: List[String] = List(zza, zzza)
请注意,这比仅要求容器是可遍历的要求更严格:对于GenTraversable的子类型,定义不符合此方式的filter方法是允许的。上面的签名将静态地防止这些类型的值被传递给multiMinBy...大概这就是您想要的行为。

一个人只需使用 TraversableLike,就可以避免整个结构类型的问题。 - Daniel C. Sobral

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接