![](/img/trans.png)
[英]Why does foldr work on infinite lists in Haskell but foldl doesn't?
[英]Why doesn't my Haskell function support infinite lists?
我不明白為什么我在教程練習中實現這個 function 不能偷懶。 運行take 4 (chunks 3 [0..])
時失敗。 請告訴我要改變什么。
我沒有做任何我認為會導致評估無限列表的事情:
-- chunks 2 [1,2,3,4] ==> [[1,2],[2,3],[3,4]]
-- take 4 (chunks 3 [0..]) ==> [[0,1,2],[1,2,3],[2,3,4],[3,4,5]]
chunks :: Int -> [a] -> [[a]]
chunks len input = chunks' len input []
chunks' :: Int -> [a] -> [[a]] -> [[a]]
chunks' _ [] output = output
chunks' len input@(input1:inputTail) output
| lengthAtLeast len input = chunks' len inputTail (output++[take len input])
| otherwise = output
where
lengthAtLeast len list = length (take len list) >= len
這是一個問題:
chunks' len input@(input1:inputTail) output
| lengthAtLeast len input = chunks' len inputTail (output++[take len input])
請注意,對於無限列表,條件lengthAtLeast...
將始終為真。 因此,我們總是陷入這種情況——它會在不產生 output 的任何部分的情況下遞歸。 output 只有在我們陷入其他情況時才會返回,但這永遠不會發生。
相反,請確保在我們遞歸之前產生結果:
chunks' len input@(input1:inputTail) output
| lengthAtLeast len input = take len input : chunks' len inputTail output
當然,這個改動后output
將永遠是空列表,我們可以去掉參數,進一步簡化代碼。
經驗法則是:在處理無限列表時,尾遞歸會導致無限循環。 您希望在遞歸之前至少生成部分結果。 通常,當您的 output 是一個像列表這樣的大型數據結構時,尾遞歸是錯誤的方法,應該逐漸生成它,以便我們可以從懶惰中受益。
除了 chi 對您的特定代碼的出色回答之外,我認為值得詳細說明尾遞歸是錯誤的方法; 您可能需要重新考慮如何處理這些功能。
看來您想使用尾遞歸編寫算法; 我認為這是因為尾遞歸更有效和/或避免堆棧溢出錯誤的常見建議。 但是,該建議來自嚴格的語言(執行尾調用消除;如果沒有此優化,則無濟於事)。 在像 Haskell 這樣的惰性語言中,尾遞歸有時會提高性能,有時會使性能變得更糟。 所以你當然不想盲目地應用累加器引入技巧。 (“累加器介紹”是您在將額外的output
參數添加到chunks'
時使用的技術的一個花哨的名稱。之所以這么稱呼,是因為隨着您在調用堆棧中的深入,您在此參數中“累積”最終結果,只返回它在基本情況下。)
特別是像這樣的尾遞歸代碼幾乎按照定義不能用於 function 應該從其(可能無限)輸入逐漸“流式傳輸”其(可能無限)結果。 通過“流”,我的意思是 function 只需要檢查其輸入的一部分,以便產生其 output 的可用部分,所以我們的調用者決定需要檢查多少輸入(通過決定多少我們的 output 它想要檢查)。
想想看; 帶有累加器參數的尾遞歸意味着遞歸調用只會返回對這個 function 的另一個調用,或者最終結果是一個 go。 調用者在強制所有這些調用層一直到基本情況之前不會得到任何東西; 這與延遲流式傳輸結果相反,並且不可能在永遠不會真正達到基本情況的無限輸入上工作。 這也意味着必須檢查整個輸入(如果輸入本身是惰性流計算的結果,則立即強制執行),並且 output 也必須完整生成並存在於 memory 中一次(或者有時更糟,一大串重擊)。
而 chi 建議的風格正是你在嚴格的語言中會避免的。 沒有累加器,並且對chunks'
的遞歸調用不在尾部 position 中。 chi's chunks'
返回一個結構化值,遞歸調用包含在其字段之一中的chunks'
(特別是使用:
構造函數生成的列表單元格,遞歸調用在第二個字段中)。 在嚴格的語言中,必須在構建包含它的列表單元之前評估遞歸調用(一直到其基本情況),這意味着為每個遞歸級別分配一個堆棧幀,並可能導致堆棧溢出。 因此,建議重構代碼以使用尾遞歸,它(如果語言具有尾調用消除)為遞歸的每個級別重用相同的堆棧幀。
然而,在 Haskell 中,惰性意味着不需要立即評估對chunks'
的遞歸調用。 它可以作為未評估的 thunk 存儲在列表單元格的第二個字段中,這意味着調用者可以立即檢查列表單元格的第一個字段,而無需在chunks'
工作。 該屬性使“流”工作,並允許處理無限的數據結構。 不需要尾遞歸來避免堆棧溢出; 正如您所見,尾遞歸會使情況變得更糟。
這種代碼結構,您將遞歸調用放在返回的數據結構的字段中,稱為受保護遞歸。 在 Haskell 中,保護遞歸通常比尾遞歸更重要; 當您想要處理無限(甚至只是“大”)數據結構時,當然更重要。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.