为什么我对自我引用懒惰序列的直觉是错误的?

时间:2014-06-24 00:53:31

标签: 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] <-- substitution
1:2:2:3:2:3:3:4:map (+1) x
...

这显然不是正在发生的事情。我可以在正确答案中看到模式。我们只是在无限流中一次移动列表中的一个元素。我认识的模式,我可以在代码中应用它。然而,它与我的懒惰评估的心理模型不符。感觉有点“神奇”。我的直觉在哪里错了?

5 个答案:

答案 0 :(得分:17)

请记住只用其定义替换某些东西。因此,无论何时展开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链。如果您只是print x,您会看到输出在一段时间后开始减速;这就是为什么。有一种更有效的方式,留作练习([1..]是作弊: - )。

N.B。这仍然比实际发生的更懒惰。 map (+1) (1 : ...)评估为(1+1) : map (+1) ...,并且只有在实际观察到数字时才会添加,只需打印或例如比较它。

Will Ness会在这篇文章中发现错误;看到评论和他的回答。

答案 1 :(得分:7)

以下是发生的事情。 懒惰是非严格的+ memoization (thunks)。我们可以通过命名在强制表达式时出现的所有临时数据来证明这一点:

λ> 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 n 1 | n <- [1..]],这是一种从根本上说效率较低的计算( O(n 2 < / sup>)而不是 O(n))。

我们可以用

来解释共享与非共享
fix g = x where x = g x        -- sharing fixpoint
x = fix ((1:) . map (1+))      --  corecursive definition

_Y g = g (_Y g)                -- non-sharing fixpoint
y = _Y ((1:) . map (1+))       --  recursive definition

尝试在GHCi的提示下打印出y,随着进展的进展显示出明显的减速。打印x流时,没有减速。

(有关类似示例,请参阅https://stackoverflow.com/a/20978114/849891。)

答案 2 :(得分: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

答案 3 :(得分:2)

让我们在数学上更多地看一下。假设

x = [1, 2, 3, 4, ...]

然后

map (+1) x = [2, 3, 4, 5, ...]

所以

1 : map (+1) x = 1 : [2, 3, 4, 5, ...] = x

这(转过身)是我们开始的等式:

x = 1 : map (+1) x

所以我们展示的是

x = [1, 2, 3, 4, ...]

是等式

的解
x = 1 : map (+1) x   -- Eqn 1

当然,接下来的问题是,是否有任何其他解决方案。事实证明,答案是否定的。这很重要,因为Haskell的评估模型有效地选择了任何此类等式的“最少定义”解。例如,如果我们改为定义x = 1 : tail x,那么1开头的任何列表将是一个解决方案,但我们实际上获取{{1其中1 : _|_表示错误或非终止。方程1不会导致这种混乱:

_|_成为方程1的任何解,所以

y

请注意,我们可以从定义中看出

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 都有take n y = take n x。也就是n

答案 4 :(得分:0)

你错了什么

按照您的评估顺序:

let x = 1:map (+1) x
1:2:map (+1) x
1:2:map (+1) [1, 2] <-- here
你正在做出错误的假设。您假设x[1, 2],因为那是您可以在那里看到的元素数量。不是。你忘了考虑最后的x需要递归计算。

实际流量

序列末尾的x需要通过递归计算自身来计算。这是实际的流程:

take 5 $ 1:map (+1) ...
take 5 $ 1:map (+1) (1:map (+1) ...
take 5 $ 1:map (+1) (1:map (+1) (1:map (+1) ...
take 5 $ 1:map (+1) (1:map (+1) (1:map (+1) (1:map (+1) ...
take 5 $ 1:map (+1) (1:map (+1) (1:map (+1) (1:map (+1) (1:map (+1) ...
take 5 $ 1:map (+1) (1:map (+1) (1:map (+1) (1:map (+1) [1 ...
take 5 $ 1:map (+1) (1:map (+1) (1:map (+1) [1, 2 ...
take 5 $ 1:map (+1) (1:map (+1) [1, 2, 3 ...
take 5 $ 1:map (+1) [1, 2, 3, 4 ...
take 5 $ [1, 2, 3, 4, 5 ...
[1, 2, 3, 4, 5]