在哈斯克尔中循环递归



我偶然发现了以下问题。假设我有一个递归函数,例如

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(,因此它看起来等同于第二个。

具体来说,对于所有底部免费列表ysstrictPrepend 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:.....1s的无限序列。这是在需要评估参数之前产生1的效果。

重要的是它head $ run <whatever>总是1.

其实不然。仅当whatever为非空列表时。传入[]时,run函数将返回一个空列表。在 WHNF 中,run的参数评估到可以决定匹配什么大小写的程度。这不会评估列表的内容或列表的尾部。

let out = run (cycle out)中,cycle函数有完全相同的问题 - 它需要模式匹配列表。由于这取决于cycle本身的结果,因此您将有一个递归计算,这是运行时抱怨的。

最新更新