如何制作尾递归函数

时间:2019-09-24 21:58:20

标签: list haskell recursion merge tail-recursion

我对如何使函数“尾递归”感到困惑。

这是我的函数,但我不知道它是否已经是尾递归。

我正在尝试合并Haskell中的两个列表。

merge2 :: Ord a =>[a]->[a]->[a]
merge2 xs [] = xs
merge2 [] ys = ys
merge2 (x:xs)(y:ys) = if y < x then y: merge2 (x:xs) ys else x :merge2 xs (y:ys)

1 个答案:

答案 0 :(得分:5)

您的函数不是尾递归的;它是受保护的递归。但是,如果要提高内存效率,则应在Haskell中使用受保护的递归。

要使调用成为尾部调用,其结果必须是整个函数的结果。此定义适用于递归和非递归调用。

例如,在代码中

f x y z = (x ++ y) ++ z

调用(x ++ y) ++ z是一个尾部调用,因为其结果是整个函数的结果。呼叫x ++ y不是尾呼叫。<​​/ p>

有关尾递归的示例,请考虑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可以建立 thunks ,或在内存中表示尚未评估代码的结构。考虑以下代码的评估:

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 -up,最后显示已经计算出的值。

您应该尝试使用带保护的递归,而不是尾递归,因为递归调用隐藏在惰性数据构造函数中。使用惰性评估,将评估表达式,直到其为弱头正常形式(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。

有用的链接: