你的函数不是尾递归的;它是递归的。但是,如果您想提高内存效率,则应该在 Haskell 中使用受保护的递归。
要使调用成为尾调用,其结果必须是整个函数的结果。此定义适用于递归和非递归调用。
例如在代码中
f x y z = (x ++ y) ++ z
调用(x ++ y) ++ z 是尾调用,因为它的结果是整个函数的结果。 x ++ y 的调用不是尾调用。
对于尾递归的示例,请考虑foldl:
foldl :: (b -> a -> b) -> b -> [a] -> b
foldl _ acc [] = acc
foldl f acc (x:xs) = foldl f (f acc x) xs
递归调用foldl f (f acc x) xs 是尾递归调用,因为它的结果是整个函数的结果。因此,它是一个尾调用,并且它是递归调用 foldl 对自身的调用。
代码中的递归调用
merge2 (x:xs) (y:ys) = if y < x then y : merge2 (x:xs) ys
else x : merge2 xs (y:ys)
不是尾递归的,因为它们没有给出整个函数的结果。调用merge2 的结果用作整个返回值的一部分,即一个新列表。 (:) 构造函数,而不是递归调用,给出了整个函数的结果。事实上,因为懒惰,(:) _ _ 会立即返回,而_ 的漏洞只会在需要时才被填充。这就是保护递归节省空间的原因。
但是,尾递归并不能保证惰性语言的空间效率。
通过惰性求值,Haskell 构建 thunk 或内存中的结构来表示尚未求值的代码。考虑对以下代码的评估:
foldl f 0 (1:2:3:[])
=> foldl f (f 0 1) (2:3:[])
=> foldl f (f (f 0 1) 2) (3:[])
=> foldl f (f (f (f 0 1) 2) 3) []
=> f (f (f 0 1) 2) 3
您可以将惰性求值视为“由外而内”发生的事情。当评估对foldl 的递归调用时,会在累加器中建立thunk。因此,在惰性语言中,带有累加器的尾递归由于延迟评估而没有空间效率(除非在进行下一次尾递归调用之前立即强制累加器,从而防止生成 thunk -up,而是在最后呈现已经计算的值)。
您应该尝试使用受保护的递归,而不是尾递归,其中递归调用隐藏在惰性数据构造函数中。使用惰性求值时,表达式会被求值,直到它们处于 weak head normal form (WHNF)。一个表达式在 WHNF 中是:
- 应用于参数的惰性数据构造函数(例如
Just (1 + 1))
- 部分应用的函数(例如
const 2)
- 一个 lambda 表达式(例如
\x -> x)
考虑map:
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
map (+1) (1:2:3:[])
=> (+1) 1 : map (+1) (2:3:[])
由于(:) 数据构造函数,表达式(+1) 1 : map (+1) (2:3:[]) 在WHNF 中,因此在此时停止计算。您的 merge2 函数也使用了受保护的递归,因此它在惰性语言中也很节省空间。
TL;DR:在惰性语言中,如果尾递归在累加器中建立 thunk,它仍然会占用内存,而受保护的递归不会建立 thunk。
有用的链接: