映射集合时避免意外删除重复项

时间:2012-04-03 11:27:54

标签: scala collections functional-programming

我非常喜欢函数式编程概念,但是当我在一个恰好是Set的集合中进行映射时(即自动删除重复项),我现在被同一个问题咬了两次。问题是在转换这样一个集合的元素之后,输出容器也是一个集合,因此删除转换输出的任何重复。

一个非常简短的REPL会议来说明这个问题:

scala> case class Person(name: String, age: Int)
defined class Person

scala> val students = Set(Person("Alice", 18), Person("Bob", 18), Person("Charles", 19))
students: scala.collection.immutable.Set[Person] = Set(Person(Alice,18), Person(Bob,18), Person(Charles,19))

scala> val totalAge = (students map (_.age)).sum
totalAge: Int = 37

我当然希望总年龄为18 + 18 + 19 = 55,但因为学生存储在Set中,所以他们的年龄< / em>在映射之后,因此18之一在年龄之前消失了。

在实际代码中,这通常更加隐蔽,更难以发现,特别是如果您编写的实用程序代码只需要Traversable和/或使用声明返回Traversable的方法的输出(其实现恰好是Set)。在我看来,这些情况几乎不可能被发现,直到/除非它们表现为一个错误。

那么,是否有任何最佳做法可以减少我对此问题的曝光?考虑map - 我是错误的 - 对一般的Traversable进行ping操作,在概念上将每个元素转换为适当的位置,而不是将转换的元素依次添加到一些新的集合中?如果我想保留这个心智模型,我应该在映射之前对所有内容调用.toStream吗?

非常感谢任何提示/建议。

更新:到目前为止,大多数答案都集中在将总和中包含重复项的机制上。在一般案例中编写代码时,我对所涉及的实践更感兴趣 - 在调用toList之前,您是否已经在每个集合上总是调用map?在调用方法之前,您是否经常检查应用程序中所有集合的具体类?等

修复已经被识别为问题的东西琐碎 - 困难的部分是防止这些错误首先蔓延。

6 个答案:

答案 0 :(得分:19)

您可能希望使用 scalaz foldMap来实现此目的,因为它适用于有Foldable类型类可用的任何内容。您案例中的用法如下所示:

persons foldMap (_.age)

foldMap的签名如下:

trait MA[M[_], A] {
  val value: M[A]

  def foldMap[B](f: A => B)(implicit f: Foldable[M], m: Monoid[B])
}

因此;只要你有一些集合CC[A],其中CC可以折叠(即遍历),A => B的函数,其中B是一个幺半群,你可以累积结果。

答案 1 :(得分:11)

不要将额外的依赖关系拖到图片中:

(0 /: students) { case (sum, s) => sum + s.age }

答案 2 :(得分:3)

您可以breakOut收集类型

scala> import collection.breakOut
import collection.breakOut

scala> val ages = students.map(_.age)(breakOut): List[Int]
ages: List[Int] = List(18, 18, 19)

然后你可以按预期加总

基于对问题的更新,防止这些类型错误的最佳实践是具有代表性数据的良好单元测试覆盖率,以及合理的API以及scala编译器如何通过map / for generators等维护源类型的知识如果你要返回一组东西,你应该明白这一点,因为返回Collection / Traversable隐藏了相关的实现细节。

答案 3 :(得分:2)

您可能希望首先使用 toIterable toList方法将集合转换为另一个数据结构。 http://www.scala-lang.org/api/current/scala/collection/immutable/Set.html

(请注意,toIterable 可能会返回任何Iterable,但根据链接的文档,参考实现不会。@ Debilski在评论中告诉我它仍会返回一个Set。 )

答案 4 :(得分:2)

如果你发现自己反复遇到同样的错误,那么你的第一个问题不是错误,而是你在重复自己。 map().sum是一个常见的用例(特别是在数据分析上下文中),值得在Traversable上使用它自己的方法。从我个人的,永不去的地方,没有它可穿越的皮条客课程。

  implicit def traversable2RichTraversable[A](t: Traversable[A]) = new {
///many many methods deleted

    def sumOf[C: Numeric](g: A => C): C = t.view.toList.map(g).sum

///many many more methods deleted

}

.view可能没有必要,但不能伤害。)

答案 5 :(得分:1)

一种笨拙但可能更快的转换方式(与明确的toList / toSeq相比)将使用collection.breakOutmore information) 类型归属

(students map (_.age))(collection.breakOut) : Seq[Int]