理解Haskell中的结构共享
在Liu和Hudak的论文“用箭头堵住空间泄漏”中,声称这会导致O(n^2)运行时行为(用于计算第n项): ,而这给了我们线性时间:理解Haskell中的结构共享,haskell,lazy-evaluation,Haskell,Lazy Evaluation,在Liu和Hudak的论文“用箭头堵住空间泄漏”中,声称这会导致O(n^2)运行时行为(用于计算第n项): ,而这给了我们线性时间: successors n = let ns = n : map (+1) ns in ns 。这句话肯定是正确的,因为我可以很容易地用GHCi进行验证。然而,我似乎无法确切理解为什么,以及在这种情况下结构共享是如何帮助的。我甚至试着写出计算第三项的两个展开式 以下是我对第一种变体的尝试: successors 1 !! 2 (1 :
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,...]
您只需计算这张地图的成本
让我们从操作角度来考虑这个问题
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
我们将计算0
,假装结果列表的脊椎在计算时被强制。我们首先将n
绑定到0
successors 0 = let ns = 0 : map (+1) ns
in ns
无论在什么地方,我们都会保留计算结果——在构造函数的(非严格)字段中,或者在let
或绑定的中,我们实际上是在存储一个thunk,它将在计算thunk时接受计算结果的值。我们可以通过引入一个新的变量名在代码中表示这个占位符。对于将map(+1)ns
放置在构造函数尾部的最终结果:
我们将引入一个名为ns0
的新变量
successors 0 = let ns = 0 : ns0 where ns0 = map (+1) ns
in ns
map (+1) (n1 : ns1) = n1 + 1 : map (+1) ns1
第一次扩展
现在让我们扩展一下
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
successors 0 = 0 : n1 : ns1
where
n1 = 0 + 1
ns1 = n1 + 1 : map (+1) ns1
我们将为计算0+1
和map(+1)ns0
引入新的变量名n1
和ns1
,最右边的:
构造函数的参数
successors 0 = 0 : ns0
where
ns0 = n1 : ns1
n1 = 0 + 1
ns1 = map (+1) ns0
successors 0 = 0 : n1 : ns1
where
n1 = 0 + 1
ns1 = n2 : ns2
n2 = n1 + 1
ns2 = map (+1) ns1
第二次扩张
我们展开map(+1)ns0
successors 0 = let ns = 0 : ns0 where ns0 = map (+1) ns
in ns
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 : ns0 where ns0 = 0 + 1 : map (+1) 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 : ns0
where
ns0 = n1 : ns1
n1 = 0 + 1
ns1 = map (+1) ns0
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
这显然是在列表的脊椎中线性增长,在thunks中线性增长,以计算列表中的值。正如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
s和n2
s,并且忘记这些变量名
successors 0 = 0 : 1 : 2 : ns2
where
ns2 = n3 : ns3
n3 = 2 + 1 -- n3 will reuse n2
ns3 = map (+1) ns2
当强制执行n3
时,它将使用已知的n2
结果(2
),并且前两个加法将不再执行。您必须对图形进行操作才能理解共享。在线性公式上这样做会让人困惑。谢谢!我想我现在明白了!谢谢你的回答!这对我帮助很大。很难选择接受哪一个。