作为练习,我尝试定义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)
即。我不再在第二个参数上使用模式匹配。为什么在这里使用head
和tail
会使ruler
终止 - 毕竟,模式匹配相当具有防御性(我只评估列表脊椎的第一个元素,不是吗?)。
答案 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
注意~
:这相当于使用y
和ys
定义head
和tail
。
例如:下面的列表未定义。
fix (\ (x:xs) -> 1:x:xs)
其中fix
是定点组合子(例如来自Data.Function
)。相比之下,这个其他列表永远重复1
:
fix (\ ~(x:xs) -> 1:x:xs)
这是因为1
是在x
和xs
之间分割列表之前生成的。
为什么强制脊椎中的第一个项会触发问题?
在推理诸如
之类的递归方程时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)
这里的问题不在于模式匹配或使用head
和tail
。问题在于如何通过将您的功能定义为
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
版本(以及您的头/尾版本)仅适用于第二个列表与第一个列表一样长或更长的列表。