标题说明了一切,真的;迭代集合同时保持循环之间的状态和基于终止条件的完成迭代以及简单地耗尽元素可能是在命令式编程中完成任何事情的最常见模式。然而,在我看来,这是一个功能性的程序员同意不谈论的东西,或者至少我从未遇到过它的成语或半标准化的名称,例如 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
s或 Stream
s,但迭代器的开销可能比转换的开销小 items
到了 Stream
,因为后者的默认实现使用下面的迭代器)。
我的问题是:
1)这个概念在函数式编程中是否有名称,如果是,那么与其实现相关的模式是什么?
2)在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)
做这些事情的功能方式是通过 尾巴 递归:
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 注释到 力 如果您正在注释的函数不是,则编译失败 尾巴 递归。我建议你尽可能多地使用它,因为它有助于避免错误并记录代码。
正确折叠,完成后 懒洋洋,可以提前终止。例如,在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
执行右折叠的函数,但递归发生在惰性值内。但是,您可能仍然遇到空间使用问题。
功能名称是Iteratee。
关于这一点有很多参考,但最好从设计结束时开始阅读 管道教程 只有当你有兴趣从那里向后工作才能看到它是如何从早期终止左侧折叠的。