热门问题
时间线
聊天
视角
Fold (高阶函数)
来自维基百科,自由的百科全书
Remove ads
在函数式编程中,折叠(fold),也称为归约(reduce)、积累(accumulate)、聚集(aggregate)、压缩(compress)或注入(inject),指称一组高阶函数,它们分析递归数据结构并通过使用给定组合运算,将递归的处理它的构成部件、建造一个返回值的结果重组起来。典型的,要向折叠提供一个组合函数,一个数据结构的顶端节点,和可能的在特定条件下使用的某些缺省值。折叠接着以系统性方式使用这个函数,进行组合这个数据结构的层级中的元素。
折叠在某种意义上是展开(unfold)的对偶,它接受一个种子值并共递归的应用一个函数,来确定如何进一步的构造一个共递归的数据结构。折叠递归的分解这个数据结构,在每个节点应用一个组合函数于它的终结值和递归结果之上,用得到这个结果替代它。折叠是catamorphism,而展开是anamorphism。
Remove ads
作为结构性变换
折叠可以视为是将数据结构的结构性构件一致性的替代为函数和值。例如在很多函数式语言中,列表是用两个原语建造的:任何列表要么是一个空列表,通常叫做nil
([]
),要么是通过将一个元素前缀于另一个列表之前来构造的,通过应用cons
函数(在Haskell中写为冒号(:)
),建立所谓的cons节点,比如 Cons(X1,Cons(X2,Cons(...(Cons(Xn,nil)))))
。可以将在列表上的折叠看作是将这个列表的末端的nil
替代为一个特殊的值,并用一个特殊函数替代每个cons
。
使用Haskell作为例子,可以用几个等式公式化出右折叠foldr
:
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f z [] = z
foldr f z (x:xs) = f x (foldr f z xs)
如果列表为空,结果是初始值z
。如果不是空,应用f
于第一个元素和折叠余下列表的结果。这种替代可以图示如下:
以一致性风格进行结构性变换的另一种方式,左折叠foldl
:
foldl :: (b -> a -> b) -> b -> [a] -> b
foldl f z [] = z
foldl f z (x:xs) = foldl f (f z x) xs
如果列表为空,结果是初始值。如果不是空,将应用f
于旧初始值和第一个元素的结果作为新初始值,折叠它和余下列表。这种替代可以图示如下:
这两个示意图展示在一个列表上的右折叠和左折叠。逐个元素的使用cons
构造的列表,每个节点的左链接是终结值,而右链接是另一个节点,右折叠之后保持这种形态。左折叠之后,承载函数的每个节点的右链接是终结值,而左链接是另一个节点。
这两个示意图还突出了如下事实:id = foldr (:) []
是在列表上的同一函数(按Lisp说法是“浅层复制”),因为替代cons
为cons
并替代nil
为nil
不改变结果。并提示了逆转一个列表的一种容易的方法:reverse = foldl (flip (:)) []
。注意这里用flip
函数将给cons
的二个参数进行了翻转。flip
只在Haskell这样的语言中需要,它翻转给foldl
的组合函数的实际参数的次序,不像在Scheme中,这里对foldl
和foldr
二者的组合函数使用相同的实际参数次序。
另一个易得的结果是,高阶函数map也可以凭借foldr
书写,通过将要作用在元素上的那个函数复合于cons
,即是:
map f = foldr ((:) . f) []
这里的点号(.)
是指示函数复合的算子。
通过演示在线性列表上的折叠函数的构造方式,就会得到启发在其他代数数据类型和结构比如各种树之上设计类似的折叠函数。我们可以写一个高阶函数,递归的将数据类型的构造子替代为所提供的函数,并将任何这个类型的常量值替代为所提供的值。这种函数一般称为catamorphism。
Remove ads
在列表上
用加法算子折叠列表[1,2,3,4,5]
会得到结果15
, 它是这个列表元素的总和。粗略近似的说,折叠将这个列表中逗号替代成了+
运算,得出了1 + 2 + 3 + 4 + 5
。
在上述的例子中,+
是结合律运算,所有最终的结果不管如何加括号都是相同的,尽管括号导致的特定计算次序是不同的。在非结合律二元运算的一般情况下,组合元素的次序可以影响最终的结果值。在列表之上,有二个面向的进行这个工作的方式:要么组合第一个元素和递归的组合余下列表的结果(叫做右折叠),要么组合递归的组合除了最后一个元素的所有元素的结果和最后一个元素(叫做左折叠)。着对应于一个二元算子要么是右结合的要么是左结合的,采用了Haskell或Prolog的术语。使用右折叠,合计将加括号为1 + (2 + (3 + (4 + 5)))
,而使用左折叠它将加括号为(((1 + 2) + 3) + 4) + 5
。
实际上,在使用右折叠的情况下有一个初始值同列表的最后一个元素组合,在使用左折叠的情况下有一个初始值同和列表的第一个元素组合,是方便和自然的。在上述的例子中,值0
(加法单比特)可以被选择为初始值,对于右折叠得到1 + (2 + (3 + (4 + (5 + 0))))
,对于左折叠得到((((0 + 1) + 2) + 3) + 4) + 5
。对于乘法,选择1
(乘法单比特)作为初始值,这将得出1 * 1 * 2 * 3 * 4 * 5 = 120 = 5!
。
在组合函数f
的类型是不对称的情况下,比如a -> b -> b
,就是说如果结果的类型不同于列表元素的类型,使用初始值是必需的。要使一个线性的应用链成为可能,使用的这个初始值的类型必须同于f
的结果的类型。不管是右折叠还是左折叠,它的类型都确定为组合函数的参数所预期的类型。如果第二个参数必须与结果相同类型,则f
可以被看作是右结合的,如果是第一个参数则为左结合的。那些使用对称类型二元运算的折叠,它的二个参数的类型和它的结果的类型必须相同。
Remove ads
在组合函数是个原群的情况下,就是说它的类型是对称的,比如a -> a -> a
,就是说结果的类型同于列表元素的类型,则可以用任意方式放置括号,因而建立嵌套子表达式的“树”,比如((1 + 2) + (3 + 4)) + 5
。如果二元运算f
是结合律的,则这个值将是良好定义的,就是对于任何加括号情况,都是相同的,尽管如何计算它的运算细节会是不同的。如果f
是非严格求值的,这可能在性能上有重大影响。
线性折叠是面向节点的,并以一致方式对列表的每个节点进行运算;而树状折叠是面向整个列表的,并以一致方式跨越节点“群”进行运算。
列表可以按树状风格来折叠,分别对于有限和不明确定义的列表二者:
foldt :: (a -> a -> a) -> a -> [a] -> a
foldt f z [] = z
foldt f z [x] = f x z
foldt f z xs = foldt f z (pairs f xs)
foldi :: (a -> a -> a) -> a -> [a] -> a
foldi f z [] = z
foldi f z (x:xs) = f x (foldi f z (pairs f xs))
pairs :: (a -> a -> a) -> [a] -> [a]
pairs f (x:y:t) = f x y : pairs f t
pairs _ t = t
在foldi
函数的情况下,为了避免在不明确定义的列表上失控求值,函数f
必须“不总是”需求它的第二个参数的值,至少不是所有都要,或者不是立即就要。
人们经常希望选择f
的单比特作为初始值z
。在没有合适的初始值的时候,例如在想要把计算它的二个参数的极大值的函数,折叠到一个非空列表之上,来得到这个列表的极大值,可以用foldr
和foldl
的变体,它们分别使用这个列表的最后一个和第一个元素作为初始值。在Haskell和其他一些语言中,它们叫做foldr1
和foldl1
,这里的“1”所指的是自动提供初始元素,和它们所应用到的列表至少要有一个元素的事实。
foldl1 f [x] = x
foldl1 f (x:y:xs) = foldl1 f (f x y : xs)
foldr1 f [x] = x
foldr1 f (x:xs) = f x (foldr1 f xs)
foldt1 f [x] = x
foldt1 f (x:y:xs) = foldt1 f (f x y : pairs f xs)
foldi1 f [x] = x
foldi1 f (x:xs) = f x (foldi1 f (pairs f xs))
Remove ads
使用Haskell解释器,折叠进行的结构性变换可以用构造一个字符串来展示:
λ> foldr (\x y -> concat ["(",x,"+",y,")"]) "0" (map show [1..13])
"(1+(2+(3+(4+(5+(6+(7+(8+(9+(10+(11+(12+(13+0)))))))))))))"
λ> foldl (\x y -> concat ["(",x,"+",y,")"]) "0" (map show [1..13])
"(((((((((((((0+1)+2)+3)+4)+5)+6)+7)+8)+9)+10)+11)+12)+13)"
λ> foldt (\x y -> concat ["(",x,"+",y,")"]) "0" (map show [1..13])
"(((((1+2)+(3+4))+((5+6)+(7+8)))+(((9+10)+(11+12))+13))+0)"
λ> foldi (\x y -> concat ["(",x,"+",y,")"]) "0" (map show [1..13])
"(1+((2+3)+(((4+5)+(6+7))+((((8+9)+(10+11))+(12+13))+0))))"
无限树状折叠,可以用Haskell的通过埃拉托斯特尼筛法的递归素数生成来演示:
primes = 2 : _Y ((3 :) . minus [5,7..] . foldi (\(x:xs) ys -> x : union xs ys) []
. map (\p-> [p*p, p*p+2*p..]))
_Y g = g (_Y g) -- = g . g . g . g . ...
这里的函数union
以本地方式运算于有序列表之上来高效的产生它们的并集,而minus
做它们的集合差。
对于有限列表,归并排序(和它的去除重复变体nubsort
)可以使用树状折叠轻易的定义为:
mergesort xs = foldt merge [] [[x] | x <- xs]
nubsort xs = foldt union [] [[x] | x <- xs]
采用的函数merge
是union
的保留重复的变体。
函数head
和last
也可以通过折叠定义为:
head = foldr (\x r -> x) (error "head: Empty list")
last = foldl (\a x -> x) (error "last: Empty list")
Remove ads
在采用惰性或非严格求值策略的场合下,foldr
将立即返回f
在列表头部和折叠余下列表的递归案例上的这个应用。因此,如果f
能够产生其结果的一部分,而不需要引用到递归案例,它在f
的“右”也就是第二个实际参数上,而余下的结果永不需要,那么递归就会停止,例如上节定义的head
函数。这允许右折叠可以运算在无限列表之上。与之相反,foldl
将立即的调用具有新参数的自身,直到它达到了列表的末端。这种尾递归可以高效的编译为循环,但是根本不能处理无限列表,它会永久递归于无限循环。
已经到达列表的末端之后,foldl
有效的建造了一个“表达式”,它是嵌套的左深化f
应用,它被提供给调用者进行求值。如果函数f
首先引用它的第二个参数,并且能够产生其结果的一部分,而不需要引用到递归案例,这里是在它的“左”也就是第一个实际参数上,那么递归会停止,例如上节定义的last
函数。这意味着尽管foldr
递归“于右侧”,它允许惰性组合函数来从左至右检查列表的元素;而反过来,尽管foldl
递归“于左侧”,它允许惰性组合函数从从右至左检查列表的元素。
逆转一个列表也是尾递归的,它可以使用rev = foldl (\ys x -> x : ys) []
实现。在有限列表上,这意味着左折叠和逆转可以复合起来以尾递归方式进行右折叠,通过修改f
使它逆转其参数的次序,例如foldr f z == foldl (flip f) z . rev
,这样尾递归的建造出的一个表达式的表示同于右折叠所建造的。
额外的中间列表结构可以通过形似传递续体风格的手段去除:foldr f z xs == foldl (\k x-> k . f x) id xs z
;另一个在也类似:foldl f z xs == foldr (\x k-> k . flip f x) id xs z
。它们也常被写为[1]:
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f z xs = foldl (\k x y -> k (f x y)) id xs z
foldl :: (b -> a -> b) -> b -> [a] -> b
foldl f z xs = foldr (\x k y -> k (f y x)) id xs z
另一个技术要点是,在使用惰性求值的左折叠的情况下,新初始化参数在进行递归调用之前是不被求值的。在达到了列表末端并尝试计算结果的巨大的表达式的时候,这可能导致堆栈溢出。为此,这种语言经常提供左折叠的更严格变体,它在进行递归调用之前强制的求值初始化参数。在Haskell中它是Data.List库里的foldl'
函数(注意这个撇号读作“prime”)。需要意识到一个事实,强制一个值用惰性数据构造子建造,其自身不会自动的强制它的构件。结合于尾递归,这种折叠接近了循环的效率,在惰性求值的最终结果是不可能或不可取的时候,确保了恒定空间运算。
Remove ads
在各种语言中
Remove ads
普遍性
折叠是多态函数。对于有如下这样定义的任何g
:
g [] = v
g (x:xs) = f x (g xs)
g
都可以表达为[20]:
g = foldr f v
还有,不动点组合子可以通过折叠实现[21],证明迭代可以被归约成折叠:
y f = foldr (\_ -> f) undefined (repeat undefined)
参见
- 聚集函数
- 迭代二元关系
- Map (高阶函数)
- Filter (高阶函数)
- 前缀和
- 递归数据类型
- 递归算子
- 递归 (计算机科学)
引用
外部链接
Wikiwand - on
Seamless Wikipedia browsing. On steroids.
Remove ads