热门问题
时间线
聊天
视角

Fold (高階函數)

来自维基百科,自由的百科全书

Remove ads

函數式編程中,摺疊(fold),也稱為歸約(reduce)、積累(accumulate)、聚集(aggregate)、壓縮(compress)或注入(inject),指稱一組高階函數,它們分析遞歸數據結構並通過使用給定組合運算,將遞歸的處理它的構成部件、建造一個返回值的結果重組起來。典型的,要向摺疊提供一個組合函數,一個數據結構的頂端節點英語Node (computer science),和可能的在特定條件下使用的某些缺省值。摺疊接着以系統性方式使用這個函數,進行組合這個數據結構的層級中的元素。

摺疊在某種意義上是展開英語Anamorphism(unfold)的對偶,它接受一個種子值並共遞歸的應用一個函數,來確定如何進一步的構造一個共遞歸的數據結構。摺疊遞歸的分解這個數據結構,在每個節點應用一個組合函數於它的終結值和遞歸結果之上,用得到這個結果替代它。摺疊是catamorphism英語catamorphism,而展開是anamorphism英語anamorphism

Remove ads

作為結構性變換

摺疊可以視為是將數據結構的結構性構件一致性的替代為函數和值。例如在很多函數式語言中,列表是用兩個原語建造的:任何列表要麼是一個空列表,通常叫做nil[]),要麼是通過將一個元素前綴於另一個列表之前來構造的,通過應用cons函數(在Haskell中寫為冒號(:)),建立所謂的cons節點英語Node (computer science),比如 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於第一個元素和摺疊餘下列表的結果。這種替代可以圖示如下:

Thumb

以一致性風格進行結構性變換的另一種方式,左摺疊foldl

foldl :: (b -> a -> b) -> b -> [a] -> b
foldl f z []     = z
foldl f z (x:xs) = foldl f (f z x) xs

如果列表為空,結果是初始值。如果不是空,將應用f於舊初始值和第一個元素的結果作為新初始值,摺疊它和餘下列表。這種替代可以圖示如下:

Thumb

這兩個示意圖展示在一個列表上的右摺疊和左摺疊。逐個元素的使用cons構造的列表,每個節點的左鏈接是終結值,而右鏈接是另一個節點,右摺疊之後保持這種形態。左摺疊之後,承載函數的每個節點的右鏈接是終結值,而左鏈接是另一個節點。

這兩個示意圖還突出了如下事實:id = foldr (:) []是在列表上的同一函數(按Lisp說法是「淺層複製」),因為替代conscons並替代nilnil不改變結果。並提示了逆轉一個列表的一種容易的方法:reverse = foldl (flip (:)) []。注意這裡用flip函數將給cons的二個參數進行了翻轉。flip只在Haskell這樣的語言中需要,它翻轉給foldl的組合函數的實際參數的次序,不像在Scheme中,這裡對foldlfoldr二者的組合函數使用相同的實際參數次序。

另一個易得的結果是,高階函數map也可以憑藉foldr書寫,通過將要作用在元素上的那個函數複合於cons,即是:

map f = foldr ((:) . f) []

這裡的點號(.)是指示函數複合英語Function composition (computer science)的算子。

通過演示在線性列表上的摺疊函數的構造方式,就會得到啟發在其他代數數據類型和結構比如各種樹之上設計類似的摺疊函數。我們可以寫一個高階函數,遞歸的將數據類型的構造子替代為所提供的函數,並將任何這個類型的常量值替代為所提供的值。這種函數一般稱為catamorphism英語catamorphism

Remove ads

在列表上

用加法算子摺疊列表[1,2,3,4,5]會得到結果15, 它是這個列表元素的總和。粗略近似的說,摺疊將這個列表中逗號替代成了+運算,得出了1 + 2 + 3 + 4 + 5

在上述的例子中,+結合律運算,所有最終的結果不管如何加括號都是相同的,儘管括號導致的特定計算次序是不同的。在非結合律二元運算的一般情況下,組合元素的次序可以影響最終的結果值。在列表之上,有二個面向的進行這個工作的方式:要麼組合第一個元素和遞歸的組合餘下列表的結果(叫做右摺疊),要麼組合遞歸的組合除了最後一個元素的所有元素的結果和最後一個元素(叫做左摺疊)。着對應於一個二元算子要麼是右結合的要麼是左結合的,採用了HaskellProlog的術語。使用右摺疊,合計將加括號為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。在沒有合適的初始值的時候,例如在想要把計算它的二個參數的極大值的函數,摺疊到一個非空列表之上,來得到這個列表的極大值,可以用foldrfoldl的變體,它們分別使用這個列表的最後一個和第一個元素作為初始值。在Haskell和其他一些語言中,它們叫做foldr1foldl1,這裡的「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]

採用的函數mergeunion的保留重複的變體。

函數headlast也可以通過摺疊定義為:

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,這樣尾遞歸的建造出的一個表達式的表示同於右摺疊所建造的。

額外的中間列表結構可以通過形似傳遞續體風格英語continuation-passing style的手段去除: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

在各種語言中

更多信息 語言, 左fold ...
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)

參見

引用

外部連結

Loading related searches...

Wikiwand - on

Seamless Wikipedia browsing. On steroids.

Remove ads