我偶然发现了以下问题。假设我有一个递归函数,例如
run :: [Int] -> [Int]
run [] = []
run (x:xs) = 1:x:run xs
重要的是它head $ run <whatever>
总是1
.这意味着 以下内容将产生输出1
:
head $ run [error "shouldnt get here"]
这证明Haskell不会在不需要的情况下评估这个论点 - 显然,因为这是Haskell最著名的功能之一。但是,如果我将结果用作参数,例如:
let out = run (cycle out) in head out
我收到运行时错误<<loop>>
.
怎么来了?
我正在使用runghc 8.4.2
.这是一个错误还是我在这里遗漏了什么?
run (error "urk!")
会出错。
相反,run [error "urk!"]
将成功返回1
.
为什么?因为run
是由案例定义的:空列表,非空列表。因此,它必须评估其参数,足以知道列表是否为空。我们不必在[error "urk!"]
中评估错误来查看列表是否为空,但是我们有 iferror "urk!"
是列表本身。
在发布的代码中,我们评估run (cycle out)
因此我们必须检查cycle out
是否为空。这触发了对cycle out
的评估,进而触发了对out
的评估(cycle
也由案例定义(。由于out
是我们定义的,我们得到了无限递归。
碰巧这种无限递归很容易被 GHC 运行时发现,GHC 运行时会发出异常<<loop>>
而不是执行非终止计算。
为了强调这个概念,请考虑这些函数
strictPrepend :: [Int] -> [Int]
strictPrepend [] = 1 : []
strictPrepend (x:xs) = 1 : x : xs
lazyPrepend :: [Int] -> [Int]
lazyPrepend xs = 1 : xs
有人可能会认为这些功能是相等的。事实上,第一个是由案例定义的,但在这两种情况下它都执行相同的操作(前置1
(,因此它看起来等同于第二个。
具体来说,对于所有底部免费列表ys
,strictPrepend ys
的结果与lazyPrepend ys
相同。
在存在底部(如error ".."
、undefined
或无限递归(的情况下,这些函数会有所不同。例如lazyPrepend undefined = 1 : undefined
,而strictPrepend undefined = undefined
,因为它在生成初始元素1
之前引发异常。
因此
let ys = strictPrepend ys
zs = lazyPrepend zs
将ys
定义为底部(无限递归(,但zs = 1:1:1:1:.....
是1
s的无限序列。这是在需要评估参数之前产生1
的效果。
重要的是它
head $ run <whatever>
总是1
.
其实不然。仅当whatever
为非空列表时。传入[]
时,run
函数将返回一个空列表。在 WHNF 中,run
的参数被评估到可以决定匹配什么大小写的程度。这不会评估列表的内容或列表的尾部。
在let out = run (cycle out)
中,cycle
函数有完全相同的问题 - 它需要模式匹配列表。由于这取决于cycle
本身的结果,因此您将有一个递归计算,这是运行时抱怨的。