[英]Why does foldr work on infinite lists in Haskell but foldl doesn't?
[英]Why does this first Haskell function FAIL to handle infinite lists, while this second snippet SUCCEEDS with infinite lists?
我有兩個Haskell函數,這兩個函數看起來和我非常相似。 但是第一個FAILS反對無限列表,第二個反對無限列表成功。 我一直在努力確定原因,但無濟於事。
兩個片段都是Prelude中“單詞”功能的重新實現。 兩者都可以對抗有限列表。
這是不處理無限列表的版本:
myWords_FailsOnInfiniteList :: String -> [String]
myWords_FailsOnInfiniteList string = foldr step [] (dropWhile charIsSpace string)
where
step space ([]:xs) | charIsSpace space = []:xs
step space (x:xs) | charIsSpace space = []:x:xs
step space [] | charIsSpace space = []
step char (x:xs) = (char : x) : xs
step char [] = [[char]]
這是處理無限列表的版本:
myWords_anotherReader :: String -> [String]
myWords_anotherReader xs = foldr step [""] xs
where
step x result | not . charIsSpace $ x = [x:(head result)]++tail result
| otherwise = []:result
注意:“charIsSpace”僅僅是Char.isSpace的重命名。
以下解釋器會話說明第一個失敗列表無效列表,而第二個失敗列表成功。
*Main> take 5 (myWords_FailsOnInfiniteList (cycle "why "))
*** Exception: stack overflow
*Main> take 5 (myWords_anotherReader (cycle "why "))
["why","why","why","why","why"]
編輯:感謝下面的回復,我相信我現在明白了。 以下是我的結論和修訂后的代碼:
結論:
所以,這是修改后的代碼。 我通常試圖避免頭部和尾部,僅僅因為它們是部分功能,而且因為我需要練習編寫相應的模式匹配。
myWords :: String -> [String]
myWords string = foldr step [""] (dropWhile charIsSpace string)
where
step space acc | charIsSpace space = "":acc
step char (x:xs) = (char:x):xs
step _ [] = error "this should be impossible"
這正確地適用於無限列表。 請注意,看不到頭部,尾部或(++)操作員。
現在,一個重要的警告:當我第一次寫出更正的代碼時,我沒有第三個等式,它與“step _ []”相匹配。 結果,我收到了關於非詳盡模式匹配的警告。 顯然,避免這種警告是個好主意。
但我以為我會遇到問題。 我上面已經提到, 將第二個arg與[]進行模式匹配是不行的 。 但我必須這樣做才能擺脫警告。
但是,當我添加“step _ []”等式時,一切都很好! 無限列表仍然沒有問題! 。 為什么?
因為修正后的代碼中的第3個等式永遠不會達到!
實際上,請考慮以下BROKEN版本。 除了我已將空列表的模式移到其他模式之上之外,它確實是相同的正確代碼:
myWords_brokenAgain :: String -> [String]
myWords_brokenAgain string = foldr step [""] (dropWhile charIsSpace string)
where
step _ [] = error "this should be impossible"
step space acc | charIsSpace space = "":acc
step char (x:xs) = (char:x):xs
我們回到堆棧溢出,因為調用step時發生的第一件事是解釋器檢查方程式1是否匹配。 為此,必須查看第二個arg是否為[]。 要做到這一點,它必須評估第二個arg。
將等式向下移動低於其他等式確保從不嘗試第三個等式,因為第一個或第二個模式總是匹配 。 第三個等式僅僅是為了省去非詳盡模式警告。
這是一次很棒的學習經歷。 感謝大家的幫助。
嘗試手動擴展表達式:
take 5 (myWords_FailsOnInfiniteList (cycle "why "))
take 5 (foldr step [] (dropWhile charIsSpace (cycle "why ")))
take 5 (foldr step [] (dropWhile charIsSpace ("why " ++ cycle "why ")))
take 5 (foldr step [] ("why " ++ cycle "why "))
take 5 (step 'w' (foldr step [] ("hy " ++ cycle "why ")))
take 5 (step 'w' (step 'h' (foldr step [] ("y " ++ cycle "why "))))
下一次擴張是什么? 你應該看到,為了模式匹配step
,你需要知道它是否是空列表。 為了找到答案,你必須至少對它進行評估。 但是第二個術語恰好是通過模式匹配的函數來減少foldr
。 換句話說,step函數無法在不調用自身的情況下查看其參數,因此您可以進行無限遞歸。
與第二個功能的擴展形成對比:
myWords_anotherReader (cycle "why ")
foldr step [""] (cycle "why ")
foldr step [""] ("why " ++ cycle "why ")
step 'w' (foldr step [""] ("hy " ++ cycle "why ")
let result = foldr step [""] ("hy " ++ cycle "why ") in
['w':(head result)] ++ tail result
let result = step 'h' (foldr step [""] ("y " ++ cycle "why ") in
['w':(head result)] ++ tail result
您可能會看到此擴展將持續到達到空間。 一旦達到空格,“頭部結果”將獲得一個值,您將產生答案的第一個元素。
我懷疑第二個函數將溢出無限字符串,不包含任何空格。 你能明白為什么嗎?
其他人已經指出了這個問題,即在生成任何輸出之前,step總是會評估它的第二個參數,但是當foldr應用於無限列表時,它的第二個參數最終將取決於步驟的另一個調用的結果。
它不必以這種方式編寫,但是你的第二個版本有點難看,因為它依賴於具有特定格式的步驟的初始參數,並且很難看出頭/尾永遠不會出錯。 (我甚至不能100%肯定他們不會!)
您應該做的是重構第一個版本,以便在至少某些情況下產生輸出而不依賴於輸入列表。 特別是我們可以看到,當字符不是空格時,輸出列表中始終至少有一個元素。 因此,延遲第二個參數的模式匹配,直到產生第一個元素。 字符是空格的情況仍然依賴於列表,但這很好,因為案例可以無限遞歸的唯一方法是傳入無限的空格列表,在這種情況下不產生任何輸出並進入循環是單詞的預期行為(它還能做什么?)
第二個版本在開始生成部分自己的答案之后才會實際評估result
。 第一個版本通過模式匹配立即評估result
。
這些無限列表的關鍵是你必須在開始要求列表元素之前產生一些東西 ,這樣輸出總是能夠“保持領先”輸入。
(我覺得這個解釋不是很清楚,但這是我能做的最好的。)
庫函數foldr
具有此實現(或類似):
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f k (x:xs) = f x (foldr f k xs)
foldr _ k _ = k
myWords_FailsOnInfiniteList
的結果取決於foldr
的結果,它依賴於step
的結果,這取決於內部foldr
的結果,這取決於...等等無限列表, myWords_FailsOnInfiniteList
將耗盡無限量的空間和時間在產生第一個字之前。
myWords_anotherReader
中的step
函數在生成第一個單詞的第一個字母之前不需要內部foldr
的結果。 不幸的是,正如Apocalisp所說,它在產生下一個單詞之前使用O(第一個單詞的長度)空間,因為當第一個單詞被生成時,尾部thunk不斷增長tail ([...] ++ tail ([...] ++ tail (...)))
。
相比之下,與之相比
myWords :: String -> [String]
myWords = myWords' . dropWhile isSpace where
myWords' [] = []
myWords' string =
let (part1, part2) = break isSpace string
in part1 : myWords part2
使用可定義為的庫函數
break :: (a -> Bool) -> [a] -> ([a], [a])
break p = span $ not . p
span :: (a -> Bool) -> [a] -> ([a], [a])
span p xs = (takeWhile p xs, dropWhile p xs)
takeWhile :: (a -> Bool) -> [a] -> [a]
takeWhile p (x:xs) | p x = x : takeWhile p xs
takeWhile _ _ = []
dropWhile :: (a -> Bool) -> [a] -> [a]
dropWhile p (x:xs) | p x = dropWhile p xs
dropWhile _ xs = xs
請注意,生成中間結果永遠不會被未來的計算所阻礙,並且只需要O(1)空間,因為結果的每個元素都可供使用。
所以,這是修改后的代碼。 我通常試圖避免頭部和尾部,僅僅因為它們是部分功能,而且因為我需要練習編寫相應的模式匹配。
myWords :: String -> [String] myWords string = foldr step [""] (dropWhile charIsSpace string) where step space acc | charIsSpace space = "":acc step char (x:xs) = (char:x):xs step _ [] = error "this should be impossible"
(旁白:你可能不在乎,但是來自圖書館的words "" == []
,但你的myWords "" = [""]
。跟尾空格的類似問題。)
看起來比myWords_anotherReader
有了很大改進,對於基於foldr
的解決方案來說非常好。
\n -> tail $ myWords $ replicate n 'a' ++ " b"
它不可能比O(n)時間更好,但myWords_anotherReader
和myWords
都在這里占用O(n)空間。 考慮到使用foldr
這可能是不可避免的。
更差,
\n -> head $ head $ myWords $ replicate n 'a' ++ " b"
myWords_anotherReader
是O(1),但新的myWords
是O(n),因為模式匹配(x:xs)
需要進一步的結果。
你可以解決這個問題
myWords :: String -> [String]
myWords = foldr step [""] . dropWhile isSpace
where
step space acc | isSpace space = "":acc
step char ~(x:xs) = (char:x):xs
~
引入了“無可辯駁的模式”。 無可辯駁的模式永遠不會失敗,也不會強制立即進行評估。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.