【问题标题】:Avoiding accidental removal of duplicates when mapping a Set映射 Set 时避免意外删除重复项
【发布时间】:2012-04-17 01:12:54
【问题描述】:

我真的很喜欢函数式编程概念,但我现在在两个不同的场合都被同一个问题所困扰,当映射到一个恰好是 Set 的集合时(即自动删除重复项)。问题是在转换这样一个集合的元素之后,输出容器也是一个集合,因此会删除 transformed 输出的任何重复项。

一个非常简短的 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 中,所以他们的年龄 在映射之后,因此18s 之一在年龄相加之前就消失了。

在实际代码中,这通常更隐蔽且更难发现,特别是如果您编写的实用程序代码仅采用 Traversable 和/或使用声明为返回 Traversable 的方法的输出(实现这恰好是一个集合)。在我看来,这些情况几乎不可能可靠地发现,除非/除非它们表现为错误。

那么,是否有任何最佳做法可以减少我遇到此问题的风险?将map-ping 考虑为在概念上将每个元素转换到位,而不是将转换后的元素依次添加到一些新集合中,我是否错了?如果我想保留这个心智模型,我应该在映射之前调用.toStream吗?

任何提示/建议将不胜感激。

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

修复已被确定为问题的问题微不足道 - 最困难的部分是从一开始就防止这些错误蔓延。

【问题讨论】:

  • +1,很好的例子。作为一个集合离开显然会带来令人惊讶的结果,所以你“必须”做一些事情,创建一个新的集合,或其他方式来确保你所见即所得
  • 完全复制:stackoverflow.com/questions/7040806/…。虽然这里的标题选择得更好。
  • 感谢您的指点,@ziggy。我确实尝试过先搜索现有的问题,但没有提出任何问题(很难得出明确的描述)。

标签: scala collections functional-programming


【解决方案1】:

为了不把多余的依赖拖到图片里:

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

【讨论】:

  • 你是不是建议不要使用.map(),以避免我的错误?
  • 您的示例根本不需要 map,使用 map 还引入了中间数据结构的创建,这在您的示例中是不必要的。所以在这个例子中,你可以通过更仔细、更有效地表达你的意图来解决问题。
  • 这很好;当我立即将映射集合转换为其他东西时(例如这里的.sum),大多数(如果不是全部)这些问题都会出现。在这种情况下,不需要存在中间集合,因此您可能是对的,完全避免 .map 是一个实用的解决方案。
  • 奇怪的是它在有和没有case 的情况下都可以工作;我同时接受 Function2 和 PartialFunction。
【解决方案2】:

您可能希望为此目的使用 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 是一个幺半群,你就可以累积一个结果。

【讨论】:

  • 我喜欢这个 - 戴夫格里菲斯的回答是正确的,但仍然非常具体。这涵盖了集合被映射然后组合成单个结果(通过Monoid)的所有情况,这实际上是我看到这个问题的地方。
  • (0 /: students) { case (sum, s) => sum + s.age }
  • (0 /: students)(_ + _.age) 更短
  • 我认为标准折叠的问题在于它在使用时不太可读(例如,作为方法的参数)。它还需要中断思维流程(对我而言)。也就是说,你认为我想对我的集合执行操作,所以你想要cc.op。我想cc.foldl(0)(_ + _.age) 会这样做
  • 是的,你 _ +_.age 更短,但我认为在简洁性和可读性之间有一个平衡。
【解决方案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 生成器等维护源类型的知识. 如果你要返回一个 Set 的东西,你应该让这一点显而易见,因为返回 Collection/Traversable 隐藏了相关的实现细节。

【讨论】:

  • 你可以说toList - 我认为关键是OP一直忘记它
【解决方案4】:

如果您发现自己反复遇到相同的错误,那么您的第一个问题不是错误,而是您在重复自己。 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 可能没有必要,但不会造成伤害。)

【讨论】:

  • sumBy 似乎是一个更符合标准库的名称。
  • +1,我会删除 .view 除非有实际的例子可以帮助。
  • 你能分享你个人的,永远不会去任何地方,没有它可遍历的皮条客课程吗?
  • 我将“sumBy”用于我的拉皮条可遍历方法,它等效(但比) traversable.toList.groupBy(f).mapValues(g).sum 。这根据某个函数 f 将一个可遍历对象拆分为多个桶,然后将某个函数 g 对每个桶中的值求和。同样,对于数据分析非常常见(实际上比 sumOf 更常见)。
  • 这门课需要做一些工作才能让公众观看(并获得工作许可才能将其公开),但我会看看我能做些什么。作为一个练习,根据你目前所见,思考它可能包含哪些好东西(然后自己写一些)。
【解决方案5】:

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

(请注意,toIterable可能返回任何 Iterable,尽管根据链接文档,参考实现不会。@Debilski 在 cmets 中通知我它仍然返回一个 Set。)

【讨论】:

  • toIterable 仍可能产生Set
  • @Debilski 链接的文档表明参考实现不会这样做。我也会稍微更新一下答案。
  • (students.toIterable map (_.age)) == Set(18, 19) 在 Scala 2.9.1-1 上。另外,我看不到文档中另有说明。
  • 为什么要创建中间集合? set.iteratorsum 一起使用就足够了。
  • @Antoras 该问题询问有关映射的问题。
【解决方案6】:

一种笨拙但可能更快的转换方法(与显式 toList/toSeq 相比)是使用 collection.breakOut (more information) 类型归属

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

【讨论】:

    猜你喜欢
    • 2021-05-07
    • 2021-03-22
    • 1970-01-01
    • 1970-01-01
    • 2016-08-13
    • 2014-02-01
    • 2022-06-14
    • 1970-01-01
    • 2016-06-11
    相关资源
    最近更新 更多