跟進:正確脫糖此 Do 塊
triples = do
[1..] >>= \z ->
[1..z] >>= \x ->
[x..z] >>= \y ->
guard (x^2 y^2 == z^2) >>= \_ ->
return (x, y, z)
我對return(x, y, z)最終如何收集和連接生成的單例串列感到困惑。
我現在明白這bind是在每個級別連接,因此沒有嵌套的大括號。
請更正我對這個塊的解釋:我們從無限串列中取出一個元素并呼叫它z。我們獲取有限[1..z串列的一個元素并呼叫它x
我們獲取有限串列的一個元素
并呼叫[x...z]它y
我們使用謂詞呼叫,guard該謂詞x^2 y^2 == z^2將[]在以下情況false下[()]回傳true[()]return[(x,y,z)]
執行順序是什么?換句話說,這是否像命令式語言中的嵌套回圈一樣執行?我對這些單例串列是如何收集和展平的感到困惑。
如果我只是執行:
[1..3] >>= \z ->
[1..z] >>= \x ->
[x..z] >>= \y ->
我只是得到一個清單。所以,我很困惑如何guard從這個大串列中獲取元素,然后從這個大串列中生成和收集單例串列。
uj5u.com熱心網友回復:
執行順序是什么?
首先,一個小題外話。由于 Haskell 是純粹的,當我們只關心結果時,執行順序就沒有那么重要了。f x g y事實上,當評估一個我們之前可能計算f x過的運算式時,g y或者反之亦然,我們會得到相同的結果,這與在具有副作用的語言中發生的情況不同。
更一般地說,您原則上可以使用不同的評估策略,結果不會改變。(學究起來,為此持有評估策略不應該陷入無限遞回或拋出例外,如error "urk"。)
話雖如此,如果我們不僅關心結果,還關心性能(空間和時間),那么評估策略就變得更加重要。foldl' ( ) 0 [1..n]如果我們的策略完全評估串列,則可以在 O(n) 空間中評估它,但 GHC 將在 O(1) 空間上評估它,因為它只會盡可能懶惰地要求串列中的數字:
foldl' ( ) 0 [1..n]
= foldl' ( ) (0 1) [2..n]
= foldl' ( ) 1 [2..n]
= foldl' ( ) (1 2) [3..n]
= foldl' ( ) 3 [3..n]
= ...
換句話說,這是否像命令式語言中的嵌套回圈一樣執行?
是的,我會說它大致相同。讓我舉一個更簡單的例子。很難完全可視化所有步驟,但希望您能大致了解。
[10,20,30] >>= \z -> [z,z 1]
= concat (map (\z -> [z,z 1]) [10,20,30])
= concat (map (\z -> [z,z 1]) (10 : [20,30]))
= concat ([10,11] : map (\z -> [z,z 1]) [20,30])
= 10 : concat ([11] : map (\z -> [z,z 1]) [20,30])
= 10 : 11 : concat ([] : map (\z -> [z,z 1]) [20,30])
= 10 : 11 : concat (map (\z -> [z,z 1]) [20,30])
= 10 : 11 : concat ([20,21] : map (\z -> [z,z 1]) [30])
= 10 : 11 : 20 : concat ([21] : map (\z -> [z,z 1]) [30])
= 10 : 11 : 20 : 21 : concat ([] : map (\z -> [z,z 1]) [30])
= 10 : 11 : 20 : 21 : concat (map (\z -> [z,z 1]) [30])
= 10 : 11 : 20 : 21 : concat ([30,31] : map (\z -> [z,z 1]) [])
= 10 : 11 : 20 : 21 : 30 : concat ([31] : map (\z -> [z,z 1]) [])
= 10 : 11 : 20 : 21 : 30 : 31 : concat ([] : map (\z -> [z,z 1]) [])
= 10 : 11 : 20 : 21 : 30 : 31 : concat (map (\z -> [z,z 1]) [])
= 10 : 11 : 20 : 21 : 30 : 31 : concat []
= 10 : 11 : 20 : 21 : 30 : 31 : []
請注意我們從未生成完整的嵌套串列[[10,11],[20,21],[30,31]],但我們確實通過延遲生成每個元素來有效地運行它,就像在嵌套 for 回圈中一樣一步一步地生成:
for z in [10,20,30]:
for x in [z,z 1]:
yield x
非常粗略地說,在 Haskell 串列中,它更像是一個“生成器”,一個物件具有.next()可用于根據需要請求串列中的下一個元素的方法。Lists-of-lists 同樣允許請求(“ .next()”)第一個串列,這將是一個允許請求第一個元素(另一個.next())的物件。如果我們連接這個串列串列,我們實際上不會同時將整個嵌套串列存盤在記憶體中,而只有一個串列串列的“生成器”和串列的單個“生成器”(在一次)。在任何時候,大致只有兩個物件是活的。
我現在會更迂腐一點。上面,當我寫長計算時,我實際上有點作弊,允許自己在中間步驟中寫[10,11],[20,21]等。實際上,這些串列只存盤了一個“生成器”:在開始連接它之前,我們并沒有真正生成整個串列,而是一次只生成一個元素,所以一切都在 O(1) 空間中運行。
讓我總結說,在我看來,理解 Haskell 的性能在一般情況下是相當困難的。嚴格語言(和命令式語言)的性能更容易理解。
懶惰非常好,有時可以使事情在幾乎令人驚訝的小空間中運行。此外,由于純度,它允許人們設計他們的代碼而忽略如何精確評估并專注于產生正確的輸出值。另一方面,評估性能通常需要一個人轉而關注評估,準確地關注那些在初始編碼期間很容易忽略的方面。這需要一點經驗。起初很難理解這foldl幾乎總是一個壞主意,雖然foldl'在某些情況下可能是一個更好的主意,而foldr對于其他人來說可能是正確的選擇。當與關聯和交換操作一起使用時( ),結果是相同的,但(空間)性能卻不同。
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/530206.html
標籤:哈斯克尔
下一篇:替換串列的多個元素
