簡體   English   中英

理解Haskell中的結構共享

[英]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) nsn+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

我們堅持計算的結果 - 在構造函數的一個(非嚴格)字段或letwhere綁定中,我們實際上存儲了一個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 + 1map (+1) ns0引入新的變量名n1ns1 ,這是最右邊的參數:構造函數。

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 + 1map (+1) ns1引入新的變量名n2ns2 ,這是最右邊的參數:構造函數。

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的加法。 我們可以(免費)用剛剛計算的值替換所有 n1n2 ,並忘記那些變量名。

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.

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