[英]Understanding Structure Sharing in Haskell
在Liu和Hudak撰寫的“用箭頭堵塞空間泄漏”一文中,聲稱這會導致O(n ^ 2)運行時行為(用於計算第n項):
successors n = n : map (+1) (successors n)
雖然這給了我們線性時間:
successors n = let ns = n : map (+1) ns
in ns
。 這句話肯定是正確的,因為我可以使用GHCi輕松驗證。 但是,我似乎無法理解為什么,以及在這種情況下結構共享如何幫助。 我甚至試圖寫出計算第三學期的兩個擴展。
這是我嘗試的第一個變體:
successors 1 !! 2
(1 : (map (+1) (successors 1))) !! 2
(map (+1) (successors 1)) !! 1
(map (+1) (1 : map (+1) (successors 1))) !! 1
2 : (map (+1) (map (+1) (successors 1))) !! 1
(map (+1) (map (+1) (successors 1))) !! 0
(map (+1) (map (+1) (1 : map (+1) (successors 1)))) !! 0
(map (+1) (2 : map (+1) (map (+1) (successors 1)))) !! 0
3 : map (+1) (map (+1) (map (+1) (successors 1))) !! 0
3
第二個:
successors 1 !! 2
(let ns = 1 : map (+1) ns in ns) !! 2
(1 : map (+1) ns) !! 2
map (+1) ns !! 1
map (+1) (1 : map (+1) ns) !! 1
2 : map (+1) (map (+1) ns) !! 1
map (+1) (map (+1) ns) !! 0
map (+1) (map (+1) (1 : map (+1) ns)) !! 0
map (+1) (2 : map (+1) (map (+1) ns)) !! 0
3 : map (+1) (map (+1) (map (+1) ns)) !! 0
3
如你所見,我的擴展看起來幾乎完全相同,似乎暗示兩者的二次行為。 不知何故,結構共享在后一個定義中設置並重用早期的結果,但它看起來很神奇。 誰能詳細說明?
松散地說:根據ns
的定義,你可以假裝已經完全評估了ns
。 所以我們實際得到的基本上是
successors n = let ns = n : map (+1) [n,n+1,n+2,n+3,n+4,...]
您只需要計算這一張map
的費用。
讓我們來看看這個操作。
ns = n : map (+1) ns
這是做什么的? 好吧,它分配一些內存來保存ns
,並在其中存儲指向值n
的(:)
構造函數和表示map (+1) ns
的“thunk”。 但是那個thunk代表ns
作為指向那個持有ns
的內存的指針! 所以我們實際上在內存中有一個循環結構。 當我們要求ns
的第二個元素時,那個thunk是強制的。 這涉及訪問ns
,但已經計算了訪問的部分。 它不需要再次計算。 這迫使的效果是,以取代map (+1) ns
與n+1:map (+1) ns'
,其中ns'
是指向的(現在已知的)第二元件ns
。 因此,當我們繼續時,我們構建一個列表,其最后一塊總是一個小圓點。
要理解這一點,我們需要map
的定義
map _ [] = []
map f (x:xs) = f x : map f xs
我們將計算successors 0
,假裝在我們計算它時強制生成列表的主干。 我們首先將n
綁定為0
。
successors 0 = let ns = 0 : map (+1) ns
in ns
我們堅持計算的結果 - 在構造函數的一個(非嚴格)字段或let
或where
綁定中,我們實際上存儲了一個thunk,它會在thunk時獲取計算結果的值被評估。 我們可以通過引入一個新的變量名來代表這個占位符。 對於map (+1) ns
的最終結果放在:
構造函數的尾部,我們將引入一個名為ns0
的新變量。
successors 0 = let ns = 0 : ns0 where ns0 = map (+1) ns
in ns
現在讓我們擴大
map (+1) ns
使用map
的定義。 我們從let
綁定中知道我們剛才寫道:
ns = 0 : ns0 where ns0 = map (+1) ns
因此
map (+1) (0 : ns0) = 0 + 1 : map (+1) ns0
當第二個項目被強制時,我們有:
successors 0 = let ns = 0 : ns0 where ns0 = 0 + 1 : map (+1) ns0
in ns
我們不再需要ns
變量了,所以我們將其刪除以清理它。
successors 0 = 0 : ns0 where ns0 = 0 + 1 : map (+1) ns0
我們將為計算0 + 1
和map (+1) ns0
引入新的變量名n1
和ns1
,這是最右邊的參數:
構造函數。
successors 0 = 0 : ns0
where
ns0 = n1 : ns1
n1 = 0 + 1
ns1 = map (+1) ns0
我們擴展map (+1) ns0
。
map (+1) (n1 : ns1) = n1 + 1 : map (+1) ns1
在列表的脊椎中的第三個項目(但還沒有它的值)被強制之后,我們有:
successors 0 = 0 : ns0
where
ns0 = n1 : ns1
n1 = 0 + 1
ns1 = n1 + 1 : map (+1) ns1
我們不再需要ns0
變量,所以我們將其刪除以清除它。
successors 0 = 0 : n1 : ns1
where
n1 = 0 + 1
ns1 = n1 + 1 : map (+1) ns1
我們將為計算n1 + 1
和map (+1) ns1
引入新的變量名n2
和ns2
,這是最右邊的參數:
構造函數。
successors 0 = 0 : n1 : ns1
where
n1 = 0 + 1
ns1 = n2 : ns2
n2 = n1 + 1
ns2 = map (+1) ns1
如果我們再次重復上一節中的步驟
successors 0 = 0 : n1 : n2 : ns2
where
n1 = 0 + 1
n2 = n1 + 1
ns2 = n3 : ns3
n3 = n2 + 1
ns3 = map (+1) ns2
這顯然在列表的脊柱中線性增長,並且在thunk中線性地增長以計算列表中保存的值。 正如dfeuer所描述的那樣,我們只是在處理列表末尾的“小圓點”。
如果我們強制列表中保留的任何值,則引用它的所有剩余thunk現在將引用已經計算的值。 例如,如果我們強制n2 = n1 + 1
,它將強制n1 = 0 + 1 = 1
,並且n2 = 1 + 1 = 2
。 列表看起來像
successors 0 = 0 : n1 : n2 : ns2
where
n1 = 1 -- just forced
n2 = 2 -- just forced
ns2 = n3 : ns3
n3 = n2 + 1
ns3 = map (+1) ns2
我們只做了兩次補充。 由於計算結果是共享的,因此永遠不會再次計數最多2的加法。 我們可以(免費)用剛剛計算的值替換所有 n1
和n2
,並忘記那些變量名。
successors 0 = 0 : 1 : 2 : ns2
where
ns2 = n3 : ns3
n3 = 2 + 1 -- n3 will reuse n2
ns3 = map (+1) ns2
當強制使用n3
,它將使用已知的n2
的結果( 2
),並且將不會再次執行前兩次添加。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.