为什么使用头/尾代替模式匹配会使评估终止?

时间:2014-08-01 10:49:37

标签: list haskell lazy-evaluation

作为练习,我尝试定义ruler

ruler :: (Num a, Enum a) => [a]

对应于标尺函数

0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2...

其中列表的n'元素(假设第一个元素对应n=1)是2的最大幂,它均分n。为了让它更有趣,我尝试实施ruler而不必进行任何可分性测试。

使用辅助功能

interleave :: [a] -> [a] -> [a]

只是简单地替换了两个给定列表中的元素,我想出了这个 - 但唉它不起作用:

interleave :: [a] -> [a] -> [a]
interleave  (x:xs) (y:ys) = x : y : interleave xs ys
interleave  _      _      = []

ruler :: (Num a, Enum a) => [a]
ruler = foldr1 interleave . map repeat $ [0..]

main :: IO ()
main = print (take 20 ruler)

该程序最终耗尽所有堆栈空间。

现在,奇怪的是,如果我调整interleave的定义以使其读取

,程序就可以正常工作
interleave (x:xs) ys = x : head ys : interleave xs (tail ys)

即。我不再在第二个参数上使用模式匹配。为什么在这里使用headtail会使ruler终止 - 毕竟,模式匹配相当具有防御性(我只评估列表脊椎的第一个元素,不是吗?)。

3 个答案:

答案 0 :(得分:16)

您正在将具有严格组合功能的foldr应用于无限列表。

简化为最小的示例,您可以在此处查看此行为:

*Main> :t const
const :: a -> b -> a
*Main> :t flip seq
flip seq :: c -> a -> c
*Main> foldr1 const [0..]
0
*Main> foldr1 (flip seq) [0..]
^CInterrupted.

正如其他答案所解释的那样,修正是让interleave懒惰。

更具体地说,这是发生的事情。首先,我们解析foldr1,用:替换外部列表的每个interleave

foldr1 interleave [[0..], [1...], ...]
= interleave [0...] (interleave [1...] (...))

为了取得进展,第一个interleave想要在产生第一个值之前评估第二个参数。但是第二个想要评估它的第二个参数,依此类推。

使用interleave的惰性定义,在评估第二个参数之前生成第一个值。特别是,interleave [1...] (...)将评估为1 : ...(这有助于第一个interleave取得进展)进一步评估内容之前。

答案 1 :(得分:11)

不同之处在于模式匹配会强制脊椎中的第一项,head/tail不会。

您可以使用 lazy 模式来实现相同的目标:

interleave  (x:xs) ~(y:ys) = x : y : interleave xs ys

注意~:这相当于使用yys定义headtail

例如:下面的列表未定义。

fix (\ (x:xs) -> 1:x:xs)

其中fix是定点组合子(例如来自Data.Function)。相比之下,这个其他列表永远重复1

fix (\ ~(x:xs) -> 1:x:xs)

这是因为1是在xxs之间分割列表之前生成的。


  

为什么强制脊椎中的第一个项会触发问题?

在推理诸如

之类的递归方程时
x = f x

x视为值“接近”的值

通常会有所帮助
undefined
f undefined
f (f undefined)
f (f (f undefined))
...

(上述直觉可以通过一些指称语义和Kleene不动点定理来精确化。)

例如,等式

x = 1 : x

定义序列的“限制”

undefined
1 : undefined
1 : 1 : undefined
...

明显收敛于重复列表。

当使用模式匹配来定义递归值时,等式变为,例如

(y:ys) = 1:y:ys

由于模式匹配,转换为

x = case x of (y:ys) -> 1:y:ys

让我们考虑其近似序列

undefined
case undefined of (y:ys) -> ....   = undefined
case undefined of (y:ys) -> ....   = undefined
...

在第二步,case发散,结果仍为undefined。 序列不接近预期的“重复的”列表,但始终是undefined

使用延迟模式,而不是

x = case x of ~(y:ys) -> 1:y:ys

我们获得序列

undefined
case undefined of ~(y:ys) -> 1:y:ys 
    = 1 : (case undefined of (y:_) -> y) : (case undefined of (_:ys) -> ys)
    = 1 : undefined : undefined      -- name this L1
case L1 of ~(y:ys) -> 1:y:ys
    = 1 : (case L1 of (y:_) -> y) : (case L1 of (_:ys) -> ys)
    = 1 : 1 : undefined : undefined  -- name this L2
case L2 of ~(y:ys) -> 1:y:ys
    = 1 : (case L2 of (y:_) -> y) : (case L2 of (_:ys) -> ys)
    = 1 : 1 : 1 : undefined : undefined

确实收敛到预期的列表。请注意,如果不提前评估case参数,“推进”惰性模式是如何“推进”的。这就是他们懒惰的原因。通过这种方式,1在执行模式匹配之前生成,使得递归定义的实体的结果非常简单。

答案 2 :(得分:4)

这里的问题不在于模式匹配或使用headtail。问题在于如何通过将您的功能定义为

来完成
interleave :: [a] -> [a] -> [a]
interleave  (x:xs) (y:ys) = x : y : interleave xs ys
interleave  _      _      = []

您严格模式匹配您的参数,也就是说,在我们选择第一个分支之前,我们需要知道它们是至少一个元素的列表。由于您在无限的列表列表中折叠此函数,我们无法真正解决这个问题,并且我们的堆栈空间不足。

要对此进行扩展(以澄清评论中提到的内容),我们第一次尝试评估interleave(在ruler中)时,我们会得到类似

interleave (repeat 0) (foldr1 interleave (map repeat [1..]))

这里的第一个参数当然与模式匹配,但是为了弄清楚第二个参数是否存在,我们必须尝试评估它,所以我们得到

interleave (repeat 1) (foldr1 interleave (map repeat [2..]))

现在我们无法对此进行评估,除非我们对第二个参数有更多了解。由于列表[2..]永远不会结束,因此这个过程可以永远持续下去。

对此的一个解决方案是对第二个参数执行延迟模式绑定:

interleave  (x:xs) ~(y:ys) = x : y : interleave xs ys

这就像第二个参数与模式匹配的承诺,所以不要担心(当然,如果不是这样,这将失败)。这意味着interleave的第一次评估可以继续进行,而不会更深入地研究重复折叠,这在多米诺骨牌般的效果中解决了这个问题。

旁注是您的interleave版本(以及您的头/尾版本)仅适用于第二个列表与第一个列表一样长或更长的列表。