Haskell 为什么我对自我参照序列的直觉是错误的?

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] <

在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] <-- 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