Haskell 为什么我对自我参照序列的直觉是错误的?
在Haskell中,我可以在GHCI中编写一个自指序列,如下所示:Haskell 为什么我对自我参照序列的直觉是错误的?,haskell,lazy-evaluation,Haskell,Lazy Evaluation,在Haskell中,我可以在GHCI中编写一个自指序列,如下所示: λ> let x = 1:map (+1) x λ> take 5 x 产生: [1,2,3,4,5] 然而,我对惰性评估的直觉告诉我,这应该在扩展期间发生 let x = 1:map (+1) x 1:2:map (+1) x 1:2:map (+1) [1, 2] <-- substitution 1:2:2:3:map (+1) x 1:2:2:3:map (+1) [1, 2, 2, 3] <
λ> let x = 1:map (+1) x
λ> take 5 x
产生:
[1,2,3,4,5]
然而,我对惰性评估的直觉告诉我,这应该在扩展期间发生
let x = 1:map (+1) x
1:2:map (+1) x
1:2:map (+1) [1, 2] <-- substitution
1:2:2:3:map (+1) x
1:2:2:3:map (+1) [1, 2, 2, 3] <-- substitution
1:2:2:3:2:3:3:4:map (+1) x
...
设x=1:map(+1)x
1:2:map(+1)x
1:2:映射(+1)[1,2]您正在映射整个列表中的+1
,这样初始1
变成n
,其中n
是延迟递归的次数,如果这有意义的话。因此,它看起来更像是这样,而不是你所想的推导:
1:... -- [1 ...]
1: map (+1) (1:...) -- [1, 2 ...]
1: map (+1) (1:map (+1) (1:...)) -- [1, 2, 3 ...]
1
在一个延迟计算的列表前面加上前缀,该列表的元素在递归的每个步骤中都会递增
因此,您可以将递归的n
第四步看作是将列表[1,2,3,…,n…]
,将其转换为列表[2,3,4,…,n+1…]
,并预先设置一个1
按照您的评估顺序:
let x = 1:map (+1) x
1:2:map (+1) x
1:2:map (+1) [1, 2] <-- here
记住,只是用它的定义来代替。因此,无论何时展开x
,都应该替换1:map(+1)x
,而不是它的“当前值”(不管这意味着什么)
我将复制杰弗里的想法,但要尊重懒惰
x = 1 : map (+1) x
take 5 x
= take 5 (1 : map (+1) x) -- x
= 1 : take 4 (map (+1) x) -- take
= 1 : take 4 (map (+1) (1 : map (+1) x) -- x
= 1 : take 4 (2 : map (+1) (map (+1) x)) -- map and (+)
= 1 : 2 : take 3 (map (+1) (map (+1) x)) -- take
= 1 : 2 : take 3 (map (+1) (map (+1) (1 : map (+1) x))) -- x
= 1 : 2 : take 3 (map (+1) (2 : map (+1) (map (+1) x))) -- map and (+)
= 1 : 2 : take 3 (3 : map (+1) (map (+1) (map (+1) x))) -- map and (+)
= 1 : 2 : 3 : take 2 (map (+1) (map (+1) (map (+1) x))) -- take
等等
练习自己用这种方式完成评估(信息量很大)
请注意,随着列表的增长,我们开始建立一个map
s链。如果您只是打印x
,您会看到输出在一段时间后开始变慢;这就是为什么。还有一种更有效的方法,作为练习(而[1..]
就是作弊:-)
注意,这仍然比实际发生的事情要少一点懒惰<代码>映射(+1)(1:…)
的计算结果为(1+1):映射(+1)…
,只有在实际观察到数字时,通过打印或比较数字,才会进行加法
Will Ness在这篇文章中发现了一个错误;查看评论和他的答案。下面是发生的情况懒惰是不严格的+记忆化(指恶棍)。我们可以通过命名强制表达式时出现的所有临时数据来说明这一点:
λ> let x = 1 : map (+1) x
>>> x = a1 : x1 -- naming the subexpressions
a1 = 1
x1 = map (+1) x
λ> take 5 x
==> take 5 (a1:x1) -- definition of x
==> a1:take 4 x1 -- definition of take
>>> x1 = map (1+) (1:x1) -- definition of x
= (1+) 1 : map (1+) x1 -- definition of map
= a2 : x2 -- naming the subexpressions
a2 = (1+) 1
x2 = map (1+) x1
==> a1:take 4 (a2:x2) -- definition of x1
==> a1:a2:take 3 x2 -- definition of take
>>> x2 = map (1+) (a2:x2) -- definition of x1
= (1+) a2 : map (1+) x2 -- definition of map
= a3 : x3 -- naming the subexpressions
a3 = (1+) a2
x3 = map (1+) x2
==> a1:a2:take 3 (a3:x3) -- definition of x2
==> a1:a2:a3:take 2 x3 -- definition of take
>>> x3 = map (1+) (a3:x3) -- definition of x2
.....
结果流中的元素a1:a2:a3:a4:…
每个元素都引用其前身:a1=1;a2=(1+)a1;a3=(1+)a2;a4=(1+)a3代码>
因此,它相当于x=iterate(1+)1
。如果不共享数据并通过反向引用重用数据(通过存储的内存化实现),那么它就相当于x=[sum$replicate n1 | n让我们从数学角度看一下
x = [1, 2, 3, 4, ...]
然后
所以
这(反过来)是我们开始的方程式:
x = 1 : map (+1) x
所以我们展示的是
x = [1, 2, 3, 4, ...]
这是方程的一个解
x = 1 : map (+1) x -- Eqn 1
当然,下一个问题是方程1是否还有其他解决方案。答案是否定的。这很重要,因为Haskell的评估模型有效地选择了“定义最少的”任何此类方程的解。例如,如果我们定义了x=1:tail x
,那么任何以1
开头的列表都将是一个解,但我们实际上会得到1:| |
,其中| |
表示错误或不终止。Eqn 1不会导致这种混乱:
设y
是方程1的任意解,所以
y = 1 : map (+1) y
请注意,我们可以从定义中看出
take 1 y = [1] = take 1 x
现在假设
take n y = take n x
然后
通过归纳,我们发现每个n
的take ny=take nx
。也就是说,y=x
在第一个例子中忘记了中的?@jeffrey这是ghci会话的有效语法。您根据评估的多少,将x
视为具有不同的值。情况并非如此。x
always具有值[1,2,3…]
。毕竟,它是一个纯值,不能依赖于时间或运行时内部状态。您可以将其视为文本替换:x=1:map(+1)x=>x=1:map(+1)(1:map(+1)x)=>1:map(+1)(1:map(+1)x))
,等等。另外,请参阅@user2407038,它没有考虑到thunks的记忆。这是一个有用的扩展,尽管称之为“评估顺序”会产生误导。这与实际计算的顺序不符。您在预结束之前强制执行整个尾部;这对无限列表不起作用(在ghci中,您可以看到它确实存在)。查看我的答案。懒惰意味着恶棍会被记忆;请查看我的答案,在这里我通过命名过渡实体来显示它。错误在您标记的前一行。1:map(+1)x
不会扩展到1:2:map(+1)x
。相反,它本质上扩展到1:map(+1)(1:tail x)
,展开为1:2:map(+1)(tail x)
,展开为1:2:map(+1)(2:tail(tail x))
,等等@WillNess,对。我只是从正确性的角度来解释它,而不是从效率的角度。你的缩减顺序没有考虑到记忆。懒惰=非严格缩减(你展示的内容)+记忆。也就是说,你展示了定义的缩减x=_Y((1:)。映射(1+),其中_yg=g(_yg)< /代码>消除共享;但实际定义是“代码> x=FIX((1:):map(1 +)),其中{FixF= x,其中x= f x} < /代码>。-考虑:在您的结果序列<代码> 1:2 3:4:…<代码>中,2是两个调用(1 +)和3(三)调用(1 +)的结果。但通过共享,进入2的增量在生成3时被重用。更正,3是两(1+)次调用的结果,4是三(1+)次调用的结果,在还原序列的最后一行可以清楚地看到。这不是发生的情况(希望如此)在GHC或任何其他sane实施中。这个答案是inco
x = 1 : map (+1) x -- Eqn 1
y = 1 : map (+1) y
take 1 y = [1] = take 1 x
take n y = take n x
take (n+1) y = take (n+1) (1 : map (+1) y)
= 1 : take n (map (+1) y)
= 1 : map (+1) (take n y)
= 1 : map (+1) (take n x)
= 1 : take n (map (+1) x)
= take (n+1) (1 : map (+1) x)
= take (n+1) x