【问题标题】:Scala transform iterative approach to a functional approach for IteratorScala 将迭代方法转换为 Iterator 的函数方法
【发布时间】:2019-04-30 10:52:20
【问题描述】:

我有以下函数处理一系列搜索事件,如果它们相关,这些事件需要在搜索流中组合在一起。

  def split(eventsIterator: Iterator[SearchFlowSearchEvent]): Iterator[SearchFlow] = {

    val sortedEventsIterator = eventsIterator.toList.sortBy(_.evTimeMillis).iterator


    val searchFlowsEvents: mutable.MutableList[mutable.MutableList[SearchFlowSearchEvent]] = mutable.MutableList()
    var currentSearchFlowEvents: mutable.MutableList[SearchFlowSearchEvent] = mutable.MutableList()
    var previousEvent: SearchFlowSearchEvent = null
    while (sortedEventsIterator.hasNext) {
      val currentEvent = sortedEventsIterator.next()

      if (isSameFlow(previousEvent, currentEvent)) {
        currentSearchFlowEvents += currentEvent
      } else {
        currentSearchFlowEvents = mutable.MutableList()
        currentSearchFlowEvents += currentEvent
        searchFlowsEvents += currentSearchFlowEvents
      }

      previousEvent = currentEvent
    }


    searchFlowsEvents
      .map(searchFlowEvents => model.SearchFlow(searchFlowEvents.toList))
      .iterator
  }

执行上面列出的事件分组的方法是迭代的(我来自 Java 世界)。

任何人都可以提供一些关于如何以功能方式实现相同结果的提示吗?

【问题讨论】:

    标签: scala functional-programming


    【解决方案1】:

    这种事情,你要使用尾递归:

            @tailrec 
            def groupEvents(
              in: Iterator[SearchFlowSearchEvent],
              out: List[List[SearchFlowSearchEvent]] = Nil
            ): List[List[SearchFlowSearchEvent]] = if (in.hasNext) {
              val next = in.next
              out match {
                case Nil => groupEvents(in, List(List(next)))
                case (head :: tail) :: rest if isSameFlow(head, next) => groupEvents(in, (next :: head :: tail) :: rest)
                case rest => groupEvents(in, List(next) :: rest)
              }
           } else out.map(_.reverse).reverse 
    

    out 包含到目前为止收集的组(以相反的顺序 - 见下文)。 如果它是空的,只需启动一个新的。否则查看第一个元素(最后一个组),并检查那里的第一个元素(最后一个事件)。如果流程相同,则将当前事件添加到该组,否则添加一个新组。重复。

    最后(如果迭代器为空),反转列表并创建流。

    在这种情况下,在 scala 中以相反的顺序组装列表是很常见的。这是因为追加到链表的末尾(或查看最后一个元素)需要线性时间,这会使整个操作变成二次的。相反,我们总是在前面添加(恒定时间),然后在最后反转(线性)。

    或者,您可以使用foldLeft 编写相同的内容,但就个人而言,我发现在这种情况下递归实现更清晰一些,尽管更长一点(在功能上,它们是等效的):

        in.foldLeft[List[List[SearchFlowSearchEvent]]](Nil) {
           case (Nil, next) => List(List(next))
           case ((head :: tail) :: rest, next) if isSameFlow(head, next) => 
              (next :: head :: tail) :: rest
           case (rest, next) => List(next) :: rest
        }.map { l => SearchFlow(l.reverse) }.reverse
    

    更新 为了解决性能问题,在 cmets 中向其他帖子提出。我在 MacBook Pro、Mac OS 10.13.5、2.9 GHz i7、16G RAM 和 scala 2.11.11(默认 REPL 设置)上对这三种解决方案进行了基准测试。

    输入是 100000 个事件,这些事件被折叠成 14551 个组。 热身后我运行每个实现大约 500 次,并取所有执行的平均时间。

    最初的实现每次运行大约需要 42 毫秒。 递归算法大约需要28ms FoldLeft 大约是 29 毫秒

    简单地对事件数组进行排序并将其转换为迭代器大约需要 20 毫秒。

    我希望这能解决程序方法是否总是比函数方法产生更好的性能的争论。 一种方法可以通过进行特定的更改和权衡来加速此实现,但简单地用循环替换递归或切换到使用可变容器并不是一种优化。

    【讨论】:

    • case ((head :: tail :: rest), next) if isSameFlow(head, next) => (next :: head :: tail) :: rest 应替换为 case ((head :: tail) :: rest, next) if isSameFlow(head, next) => (next :: head :: tail) :: rest 以避免编译问题。
    • 我发现您的两个解决方案都非常优雅。感谢您对此的帮助。我选择了foldLeft 解决方案,因为在我看来它更容易掌握。 (我仍然需要彻底记录它以帮助我的代码的维护者)。
    【解决方案2】:

    据我所知,收藏库中没有简单的内置解决方案。正如@Dima 所说,您应该为此使用递归。

    请注意,如果您非常关心性能,那么使用 varmutable 集合的初始解决方案可能是最快的。只要你有充分的理由并且只要突变保持在特定方法的本地,可变性就很好。

    为了让我自己非常清楚,我鼓励您对其进行微优化,除非您有一个基准表明这可以以不可忽略的方式帮助您的应用程序的性能。

    【讨论】:

    • 队列在访问时反转所有内容。您不会以这种方式避免任何成本。值得的是 queue.last 是线性的,所以,你正在做的是 O(N^2)。避免成本的一种方法是使用预先分配的数组而不是列表。但这不太可能在实践中对性能产生任何显着影响。无论您如何切片,它仍然是 O(N)。不,最初的实现不是“更好的性能”。这里没有可变性的“充分理由”。
    • 关于性能,我已经看到使用循环和变量而不是递归调用和不可变结构的数量级改进。我不建议一直这样做,而且我没有对这种特殊情况进行基准测试,但它确实发生了,根据具体情况,性能可能是一个很好的理由。不过,FP 对 99% 的情况都适用。
    • 摊销,是的。如果您使用队列来做很多enqueue/dequeue 对,那么平均而言,它将是恒定时间。但是如果你只是做了很多入队,然后将所有内容都出队一次,它会在两者之间反转。请注意,“摊销恒定时间”备注适用于.last。因此,您的实现实际上是二次的,而不是线性的。是的,可以提出一个案例,其中循环和变量更快。尽管没有实际问题需要解决,但过早地提出此类优化建议是错误
    • @Dima 是的,你说得对,我的错。我将编辑以仅保留有关表演的评论。我同意不变性应该是默认值,但是建议这可能是一个问题并没有错误。如果您认为我说可变版本肯定更好,请再次阅读我的答案。每次我为此运行基准测试时,可变版本都名列前茅,这在 JVM 上完全有意义。这并不意味着我在任何地方都编写可变代码,因为我更看重调试时间,而不是微优化将节省 CPU 的几毫秒。
    • 好的,我不太喜欢喂巨魔,所以这将是我的最后一条消息。过早优化适用于没有先进行基准测试的优化。我的回答是,可变版本可能会更快,根据我的经验,无论你喜欢与否,这是正确的。同样,我只是为了完整性而添加它。然后我的 cmets 重申这很少需要。你的回答应该被接受,我什至投了赞成票。请记住,在生活中的编程中,经常会有取舍,而且并不是你想要的那样非黑即白。
    猜你喜欢
    • 2021-08-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-29
    • 2015-06-16
    • 2017-08-01
    • 2015-01-08
    • 1970-01-01
    相关资源
    最近更新 更多