簡體   English   中英

foldl與具有無限列表的foldr行為

[英]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'將在無限列表中掛起無限循環(不會導致堆棧溢出)。

Haskell wiki也有一個討論這個問題的頁面

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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM