在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
如你所见,我的扩展看起来几乎完全相同,似乎暗示了两者的二次行为。不知何故,结构共享在后一个定义中设置并重用早期的结果,但它看起来很神奇。任何人都可以详细说明吗?
答案 0 :(得分:13)
松散地说:在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
的(现在已知的)第二个元素的指针。因此,当我们继续时,我们建立一个列表,其最后一块总是一个小圆点。
答案 1 :(得分:10)
要理解这一点,我们需要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
我们将为计算n1
和ns1
引入新变量名0 + 1
和map (+1) ns0
,这是最右边:
构造函数的参数。
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
我们将为计算n2
和ns2
引入新变量名n1 + 1
和map (+1) ns1
,这是最右边:
构造函数的参数。
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所描述的那样,我们只会处理"小圆形位"在列表的 end 。
如果我们强制列表中保留的任何值,则引用它的所有剩余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
)的结果,并且前两次添加将永远不会再次完成。