有没有一个概念用于折叠'折叠'或者用累加器找到'在函数式编程中?

时间:2016-04-09 15:06:11

标签: scala functional-programming

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

(一个更具功能性的变体将使用ListStream,但迭代器的开销可能比将items转换为Stream的开销要小得多,因为它的默认实现是后者在下面使用了一个迭代器)。

我的问题是:

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

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

4 个答案:

答案 0 :(得分:10)

scala纯粹主义者对此不以为然,但你可以使用这样的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)

答案 1 :(得分:6)

执行此类操作的功能方法是通过Tail Recursion

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并且没有任何开销,因为 tail 递归会从编译器转换为迭代。

此外,模式匹配通常用于分解序列。例如,如果您有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)
  }
}

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

答案 2 :(得分:2)

此函数的名称为Iteratee。

关于此的参考文献很多,但是最好还是从阅读Pipes Tutorial开始设计的起点,并且只有在您有兴趣从那里进行倒退看看它是如何产生的时候才开始。尽早终止左折。

答案 3 :(得分:1)

正确折叠,当懒惰时,可以提前终止。例如,在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函数,但递归发生在一个惰性值内。但是,您可能仍然遇到空间使用问题。