问题 函数式编程中是否存在“折叠与折断”或“与累加器一起查找”的概念?


标题说明了一切,真的;迭代集合同时保持循环之间的状态和基于终止条件的完成迭代以及简单地耗尽元素可能是在命令式编程中完成任何事情的最常见模式。然而,在我看来,这是一个功能性的程序员同意不谈论的东西,或者至少我从未遇到过它的成语或半标准化的名称,例如 mapfoldreduce

我经常在scala中使用followinig代码:

implicit class FoldWhile[T](private val items :Iterable[T]) extends AnyVal {
    def foldWhile[A](start :A)(until :A=>Boolean)(op :(A, T)=>A) :A = {
        if (until(start)) start
        else {
            var accumulator = start
            items.find{ e => accumulator = op(accumulator, e); until(accumulator) }
            accumulator
        }

    }

}

但它很难看。每当我尝试一种更具声明性的方法时,我会得到更长,几乎肯定更慢的代码,类似于:

Iterator.iterate((start, items.iterator)){
    case (acc, i) if until(acc) => (acc, i)
    case (acc, i) if i.hasNext => (op(acc, i.next()), i)
    case x => x
}.dropWhile {
    case (acc, i) => !until(acc) && i.hasNext
}.next()._1

(将使用更具功能性的变体 Lists或 Streams,但迭代器的开销可能比转换的开销小 items 到了 Stream,因为后者的默认实现使用下面的迭代器)。

我的问题是:

1)这个概念在函数式编程中是否有名称,如果是,那么与其实现相关的模式是什么?

2)在scala中实现它的最佳方法(即简洁,通用,懒惰和最少开销)是什么?


11592
2018-04-09 15:06


起源

我永远不明白为什么这没有标准的实现。是。 tail recursion是这样做的方法,但它有点难看(并且需要一个帮助函数,需要找到一个名称,这对我来说似乎总是有点代码味道)。 。mapUntil 和 foldLeftUntil 对我来说似乎有用的东西...... - The Archetypal Paul


答案:


这是斯卡拉纯粹主义者不赞成的,但你可以使用 return 声明是这样的:

 def foldWhile[A](zero: A)(until:A => Boolean)(op:  (A,T) => A): A = items.fold(zero) {
      case (a, b) if until(a) => return a
      case (a,b) => op(a, b)
}

或者,如果你是那些皱眉的人之一,并且想要一个没有脏的命令式技巧的纯功能解决方案,你可以使用懒惰的东西,比如迭代器或流:

items
  .toStream // or .iterator - it doesn't really matter much in this case
  .scanLeft(zero)(op)
  .find(until)

9
2018-04-09 16:14



哈哈,我忘了scala有一个返回语句:)我认为在一个双行方法中它是可以原谅的,实际上比使用var更有效和可读。引用来自其中一个scala集合的消息来源评论“这是一个不完美的世界,但至少我们可以弥补不完美”。扫描的一个班轮当然也值得记住 - 谢谢! - Turin


做这些事情的功能方式是通过 尾巴 递归

implicit class FoldWhile[T](val items: Iterable[T]) extends AnyVal {

  def foldWhile[A](zero: A)(until: A => Boolean)(op: (A, T) => A): A = {
    @tailrec def loop(acc: A, remaining: Iterable[T]): A =
      if (remaining.isEmpty || !until(acc)) acc else loop(op(acc, remaining.head), remaining.tail)

    loop(zero, items)
  }
}

使用递归,您可以在每个步骤决定是否要继续使用 break 没有任何开销,因为 尾巴 递归从编译器转换为迭代。

此外,模式匹配通常用于分解序列。例如,如果你有一个 List 你可以这样做:

implicit class FoldWhile[T](val items: List[T]) extends AnyVal {

  def foldWhile[A](zero: A)(until: A => Boolean)(op: (A, T) => A): A = {
    @tailrec def loop(acc: A, remaining: List[T]): A = remaining match {
      case Nil              => acc
      case _ if !until(acc) => acc
      case h :: t           => loop(op(acc, h), t)
    }

    loop(zero, items)
  }
}

斯卡拉有 @ scala.annotation.tailrec 注释到  如果您正在注释的函数不是,则编译失败 尾巴 递归。我建议你尽可能多地使用它,因为它有助于避免错误并记录代码。


5
2018-04-09 15:22



在纯函数式语言中,这可能是最实用的答案。我没有在目的中使用递归,因为 tail 对于某些集合来说可能是一个非常慢的操作(即使Vector远非理想),我相信(可能是错误的)通常在非具体集合类型上进行任何迭代的首选方法是使用内置方法;任何外部迭代,包括递归,都需要经过一个额外的迭代器/流转换层,并通过每个方法调用进行验证,这通常是在执行回调时进行的。 - Turin
坦率地说,我暗中希望听到一些可以打开新视野的类别理论术语(例如Monoids和reduce);) - Turin
@Turin你说的是递归的具体实现取决于集合。我向你展示的那个是带有快速头尾分解的类似列表的集合。但一般来说,递归是让你决定是否要在每一步停止的原因,你如何实现这个步骤取决于你(例如头尾分解,一个可变的迭代器,等等)。我也会等待一些“减少幺半群”,我绝对不是一个铁杆功能程序员:D - Giovanni Caporaletti
另请注意,“内置”方法在内部使用过程代码。如果你想要相同的效率和零开销/中间集合,你总是可以使用while / break,但这与使用尾递归基本相同。 - Giovanni Caporaletti


正确折叠,完成后 懒洋洋,可以提前终止。例如,在Haskell中,您可以编写 find 函数(返回满足谓词的列表的第一个元素) foldr

find :: (a -> Bool) -> [a] -> Maybe a
find p = foldr (\a r -> if p a then Just a else r) Nothing

-- For reference:
foldr :: (a -> r -> r) -> r -> [a] -> r
foldr _ z [] = []
foldr f z (a:as) = f a (foldr f z as)

当你尝试,比方说,会发生什么 find even [1..]? (请注意,这是一个无限的列表!)

find even [1..]
  = foldr (\a r -> if even a then Just a else r) Nothing [1..]
  = if even 1 
    then Just 1 
    else foldr (\a r -> if even a then Just a else r) Nothing ([2..])
  = if False 
    then Just 1 
    else foldr (\a r -> if even a then Just a else r) Nothing ([2..])
  = foldr (\a r -> if even a then Just a else r) Nothing ([2..])
  = if even 2 
    then Just 2 
    else foldr (\a r -> if even a then Just a else r) Nothing ([3..])
  = if True 
    then Just 2 
    else foldr (\a r -> if even a then Just a else r) Nothing ([3..])
  = Just 2

懒惰意味着我们折叠的功能(\a r -> if even a then Just a else r)决定是否强迫 r 论证 - 评价要求我们根据名单递减的论点。所以当 even 2 评估为 True,我们挑选的分支 if ... then ... else ... 这会丢弃从列表尾部计算出的结果 - 这意味着我们从不评估它。 (它也可以在恒定的空间中运行。虽然急剧的功能语言的程序员学会避免 foldr由于空间和终止问题,在懒惰的语言中并不总是如此!)

这当然取决于Haskell被懒惰评估的事实,但是应该可以用像Scala这样的热切语言模拟它 - 我知道它有一个 lazy val 可能对此有用的功能。看起来你需要写一个 lazyFold 执行右折叠的函数,但递归发生在惰性值内。但是,您可能仍然遇到空间使用问题。


1
2018-04-12 19:18



谢谢,它肯定让我看到了折叠器的用处;开箱即用它比scala中的foldl更有用,缺少haskell的懒惰但是使用stackoverlow炸弹xoming。虽然它肯定会比Haskell更难看,但它当然可以用scala模拟。 - Turin
好的,早上回到它,现在我觉得它不会做我的初始脚本,因为stop谓词接受集合的元素而不是累加器。我不认为有一种方法可以同时吃蛋糕并用折叠器吃它,即两者都在某个元素上具有累加器的值并且避免对其余元素进行评估。沿着堆栈走下去,我们没有关于累加器结果的信息,回过头来我们已经遍历了整个集合,并且无论如何都必须通过整个堆栈。 - Turin


功能名称是Iteratee。

关于这一点有很多参考,但最好从设计结束时开始阅读 管道教程 只有当你有兴趣从那里向后工作才能看到它是如何从早期终止左侧折叠的。


0
2017-07-31 20:55