[英]Left and Right Folding over an Infinite list
我對“ 了解您的Haskell”一文的以下段落有疑問(很棒的imo書,不是廢話):
一個很大的不同是,右折疊在無限列表上起作用,而左折疊則不行! 簡而言之,如果您在某個時間點取一個無限的列表,然后將其從右側折疊起來,最終您將到達列表的開頭。 但是,如果您在某個點取一個無限的列表,然后嘗試將其從左向上折疊,那么您將永遠無法結束!
我就是不明白這一點。 如果您要獲取一個無限列表,然后嘗試從右側將其折疊起來,那么您就必須從無窮大的那一點開始,這幾乎是沒有發生的(如果有人知道您可以使用的語言,請告訴:p )。 至少,您必須根據Haskell的實現從那里開始,因為在Haskell foldr和foldl中,不需要使用確定在列表中應該從何處開始折疊的參數。
我同意引號iff foldr和foldl的參數確定了它們應該在列表中的何處開始折疊,因為這是有意義的,如果您采用無限列表並從已定義的索引開始就開始折疊,則它最終將終止,而不會不管您從左折處開始, 您將向無限折疊。 但是foldr和foldl 不接受此參數,因此引號沒有意義。 在Haskell中,無限列表上的左折和右折都不會終止 。
我的理解正確嗎?或者我缺少什么?
這里的關鍵是懶惰。 如果用於折疊列表的函數很嚴格,則給定無限列表,向左折疊或向右折疊都不會終止。
Prelude> foldr (+) 0 [1..]
^CInterrupted.
但是,如果嘗試折疊不太嚴格的功能,則會得到終止結果。
Prelude> foldr (\x y -> x) 0 [1..]
1
您甚至可以得到一個無限數據結構的結果,因此盡管它在某種意義上不會終止,但仍然可以產生可以延遲使用的結果。
Prelude> take 10 $ foldr (:) [] [1..]
[1,2,3,4,5,6,7,8,9,10]
但是,這不適用於foldl
,因為您將永遠無法評估最外部的函數調用,無論是否延遲。
Prelude> foldl (flip (:)) [] [1..]
^CInterrupted.
Prelude> foldl (\x y -> y) 0 [1..]
^CInterrupted.
請注意,左右折疊之間的關鍵區別不是列表的遍歷順序(始終從左到右),而是嵌套的結果函數應用程序的順序。
使用文件foldr
,它們嵌套在“內部”
foldr fy (x:xs) = fx (foldr fy xs)
在這里,第一次迭代將導致f
的最外層應用。 因此, f
有機會變得懶惰,以便不總是對第二個參數進行求值,或者它可以產生數據結構的某些部分而不會強迫其第二個參數。
使用foldl
,它們嵌套在“外部”
foldl fy (x:xs) = foldl f (fyx) xs
在這里,我們無法評估任何東西,直到我們到達f
的最外層應用為止,無論f
是否嚴格,在無限列表的情況下我們都無法達到。
關鍵詞是“在某個時候”。
如果您在某個時間點取了一個無限列表,然后將其從右側折疊起來,最終您將到達列表的開頭。
因此,您是對的,您不可能從無限列表的“最后一個”元素開始。 但是作者的觀點是:假設可以。 只需在遠處選擇一個點(對於工程師來說,這“足夠接近”到無窮大)並開始向左折疊。 最終,您最終會在列表的開頭。 對於左折,情況並非如此,如果您在那兒選擇一個點waaaay(並將其稱為“足夠接近”到列表的開頭),然后開始向右折,您還有無限的路要走。
因此,訣竅在於,有時您無需達到無窮大。 您可能甚至不需要去那里。 但是您可能不知道需要走多遠,在這種情況下,無限列表非常方便。
簡單的示例是foldr (:) [] [1..]
。 讓我們進行折疊。
回想一下,文件foldr fz (x:xs) = fx (foldr fz xs)
。 在無限列表上, z
實際上並不重要,因此我只是將其保留為z
而不是[]
,這會使插圖混亂
foldr (:) z (1:[2..]) ==> (:) 1 (foldr (:) z [2..])
1 : foldr (:) z (2:[3..]) ==> 1 : (:) 2 (foldr (:) z [3..])
1 : 2 : foldr (:) z (3:[4..]) ==> 1 : 2 : (:) 3 (foldr (:) z [4..])
1 : 2 : 3 : ( lazily evaluated thunk - foldr (:) z [4..] )
看看文件foldr
如何折疊,盡管從理論上說是從右側折疊的,但實際上是從左側開始將結果列表中的各個元素都拉出來了嗎? 因此,如果您從此列表中take 3
,則可以清楚地看到它將能夠產生[1,2,3]
並且無需進一步評估折數。
請記住,在Haskell中,由於延遲計算,您可以使用無限列表。 因此, head [1..]
僅為1,而head $ map (+1) [1..]
為2,即使`[1 ..]無限長。 如果沒有得到,請停下來玩一會兒。 如果您知道,請繼續閱讀...
我認為,造成混淆的部分原因是, foldl
和foldr
總是從一側或另一側開始,因此您無需給出長度。
foldr
定義很簡單
foldr _ z [] = z
foldr f z (x:xs) = f x $ foldr f z xs
為什么這會在無限列表上終止,請嘗試
dumbFunc :: a -> b -> String
dumbFunc _ _ = "always returns the same string"
testFold = foldr dumbFunc 0 [1..]
在這里,我們將一個“”(因為值無關緊要)和無窮自然數列表傳遞到文件foldr
。 這會終止嗎? 是。
它終止的原因是因為Haskell的評估等同於惰性術語重寫。
所以
testFold = foldr dumbFunc "" [1..]
變為(允許模式匹配)
testFold = foldr dumbFunc "" (1:[2..])
與(根據我們對fold的定義)相同
testFold = dumbFunc 1 $ foldr dumbFunc "" [2..]
現在通過dumbFunc
的定義,我們可以得出結論
testFold = "always returns the same string"
當我們具有執行某些功能但有時很懶的功能時,這會更有趣。 例如
foldr (||) False
用於查找列表是否包含任何True
元素。 我們可以使用它來定義高階函數,只要且僅當傳入的函數對於列表的某些元素為true時, any
函數均返回True
any :: (a -> Bool) -> [a] -> Bool
any f = (foldr (||) False) . (map f)
關於惰性評估的好處是,當遇到第一個元素e
時,它將停止,例如fe == True
另一方面, foldl
並非如此。 為什么? 好吧,一個非常簡單的foldl
看起來像
foldl f z [] = z
foldl f z (x:xs) = foldl f (f z x) xs
現在,如果我們嘗試上面的示例會發生什么
testFold' = foldl dumbFunc "" [1..]
testFold' = foldl dumbFunc "" (1:[2..])
現在變成:
testFold' = foldl dumbFunc (dumbFunc "" 1) [2..]
所以
testFold' = foldl dumbFunc (dumbFunc (dumbFunc "" 1) 2) [3..]
testFold' = foldl dumbFunc (dumbFunc (dumbFunc (dumbFunc "" 1) 2) 3) [4..]
testFold' = foldl dumbFunc (dumbFunc (dumbFunc (dumbFunc (dumbFunc "" 1) 2) 3) 4) [5..]
等等等等。 我們永遠無法到達任何地方,因為Haskell總是首先評估最外部的函數(簡而言之就是惰性評估)。
一個很酷的結果是,您可以在foldr
之外實現foldl
,反之亦然。 這意味着在某種程度上, foldr
是所有高階字符串函數中最基礎的,因為它是我們用於實現幾乎所有其他函數的函數。 有時您仍可能需要使用foldl
,因為您可以遞歸實現foldl
tail,並從中獲得一些性能提升。
Haskell Wiki上有很好的解釋。 它顯示了使用不同類型的折疊和累加器功能的逐步還原。
您的理解是正確的。 我想知道作者是否正在嘗試談論Haskell的惰性評估系統(在該系統中,您可以將無限列表傳遞給不包括fold的各種函數,並且僅評估需要多少才能返回答案)。 但我同意您的觀點,即作者在描述該段中的內容時做得不好,並且說錯了。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.