[英]Why does foldr work on infinite lists in Haskell but foldl doesn't?
[英]foldl versus foldr behavior with infinite lists
此問題中 myAny函數的代碼使用foldr。 當謂詞滿足時,它會停止處理無限列表。
我用foldl重寫了它:
myAny :: (a -> Bool) -> [a] -> Bool
myAny p list = foldl step False list
where
step acc item = p item || acc
(請注意,步驟函數的參數已正確反轉。)
但是,它不再停止處理無限列表。
我試圖在Apocalisp的答案中跟蹤函數的執行情況:
myAny even [1..]
foldl step False [1..]
step (foldl step False [2..]) 1
even 1 || (foldl step False [2..])
False || (foldl step False [2..])
foldl step False [2..]
step (foldl step False [3..]) 2
even 2 || (foldl step False [3..])
True || (foldl step False [3..])
True
但是,這不是函數的行為方式。 這怎么了?
fold
的差異似乎是混淆的常見原因,因此這里有一個更概括的概述:
考慮用一些函數f
和種子z
折疊n個值[x1, x2, x3, x4 ... xn ]
的列表。
foldl
是: f ( ... (f (f (f (fz x1) x2) x3) x4) ...) xn
foldl (flip (:)) []
反轉列表。 foldr
是: f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
f
應用於下一個值以及折疊列表其余部分的結果。 foldr (:) []
返回一個未更改的列表。 這里有一個稍微微妙的點,有時會引起人們的注意:因為foldl
是向后的 ,所以f
每個應用都被添加到結果的外部 ; 因為它是懶惰的 ,所以在需要結果之前不會對任何內容進行評估。 這意味着要計算結果的任何部分,Haskell首先遍歷構建嵌套函數應用程序表達式的整個列表 ,然后評估最外層函數,根據需要評估其參數。 如果f
總是使用它的第一個參數,這意味着Haskell必須一直遞歸到最里面的術語,然后向后計算f
每個應用程序。
這顯然與大多數功能程序員所熟悉和喜愛的高效尾遞歸相差甚遠!
實際上,即使foldl
在技術上是尾遞歸的,因為整個結果表達式是在計算任何東西之前構建的,所以foldl
會導致堆棧溢出!
另一方面,考慮foldr
。 它也很懶惰,但因為它向前運行, f
每個應用程序都會添加到結果的內部 。 因此,為了計算結果,Haskell構造了一個單獨的函數應用程序,其第二個參數是折疊列表的其余部分。 如果f
在其第二個參數(例如數據構造f
是惰性的,則結果將是遞增延遲的 ,折疊的每個步驟僅在計算需要它的結果的某些部分時計算。
因此,當foldl
沒有時,我們可以看到為什么foldr
有時會在無限列表上工作:前者可以懶惰地將無限列表轉換為另一個惰性無限數據結構,而后者必須檢查整個列表以生成結果的任何部分。 另一方面,具有立即需要兩個參數的函數的foldr
(+)
例如(+)
)與foldl
一樣工作(或者更確切地說,不起作用),在評估它之前構建一個巨大的表達式。
所以需要注意的兩點是:
foldr
可以將一個惰性遞歸數據結構轉換為另一個。 您可能已經注意到它聽起來像foldr
可以做任何foldl
都可以,以及更多。 這是真的! 事實上, foldl幾乎沒用!
但是如果我們想通過折疊大(但不是無限)列表來產生非惰性結果呢? 為此,我們需要嚴格的折疊 , 標准庫可以提供 :
foldl'
是: f ( ... (f (f (f (fz x1) x2) x3) x4) ...) xn
foldl' (flip (:)) []
反轉列表。 因為foldl'
是嚴格的 ,為了計算結果,Haskell將在每一步評估 f
,而不是讓左參數累積一個巨大的,未評估的表達式。 這給了我們想要的通常,有效的尾遞歸! 換一種說法:
foldl'
可以有效地折疊大型列表。 foldl'
將在無限列表中掛起無限循環(不會導致堆棧溢出)。 myAny even [1..]
foldl step False [1..]
foldl step (step False 1) [2..]
foldl step (step (step False 1) 2) [3..]
foldl step (step (step (step False 1) 2) 3) [4..]
等等
直觀地說, foldl
總是在“外部”或“左”上,因此它首先被擴展。 廣告無限。
你可以在Haskell的文檔中看到 ,foldl是尾遞歸的,如果傳遞一個無限列表將永遠不會結束,因為它在返回值之前調用自己的下一個參數...
我不知道Haskell,但在Scheme中, fold-right
將始終在列表的最后一個元素上“行動”。 因此對於循環列表(與無限循環列表相同)將不起作用。
我不確定fold-right
可以寫為tail-recursive,但是對於任何循環列表,你應該得到堆棧溢出。 fold-left
OTOH通常使用尾遞歸實現,如果不及早終止,它將陷入無限循環。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.