标题说明了一切,真的;迭代集合同时保持循环之间的状态和基于终止条件的完成迭代以及简单地耗尽元素可能是在命令式编程中完成任何事情的最常见模式。然而,在我看来,这是一个功能性的程序员同意不谈论的东西,或者至少我从来没有遇到过它的成语或半标准化的名称,例如map
,fold
,reduce
等。
我经常在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
(一个更具功能性的变体将使用List
或Stream
,但迭代器的开销可能比将items
转换为Stream
的开销要小得多,因为它的默认实现是后者在下面使用了一个迭代器)。
我的问题是:
1)这个概念在函数式编程中是否有名称,如果是,那么与其实现相关的模式是什么?
2)在scala中实现它的最佳方法(即简洁,通用,懒惰和最少开销)是什么?
答案 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
函数,但递归发生在一个惰性值内。但是,您可能仍然遇到空间使用问题。