从一个简单的任务开始,例如将列表中的项目从 'a 映射到 'b。我们想写一个有签名的函数
val map: ('a -> 'b) -> 'a list -> 'b list
在哪里
map (fun x -> x * 2) [1;2;3;4;5] == [2;4;6;8;10]
从非尾递归版本开始:
let rec map f = function
| [] -> []
| x::xs -> f x::map f xs
这不是尾递归,因为函数在进行递归调用后仍有工作要做。 :: 是 List.Cons(f x, map f xs) 的语法糖。
如果我将最后一行改写为| x::xs -> let temp = map f xs; f x::temp,函数的非递归性质可能会更加明显——显然它在递归调用之后工作。
使用累加器变量使其尾递归:
let map f l =
let rec loop acc = function
| [] -> List.rev acc
| x::xs -> loop (f x::acc) xs
loop [] l
这是我们在变量acc 中建立一个新列表。由于列表是反向构建的,因此我们需要在将输出列表返回给用户之前将其反向。
如果你有点心不在焉,你可以使用继续传递来更简洁地编写代码:
let map f l =
let rec loop cont = function
| [] -> cont []
| x::xs -> loop ( fun acc -> cont (f x::acc) ) xs
loop id l
由于对 loop 和 cont 的调用是最后调用的函数,无需额外工作,因此它们是尾递归的。
之所以有效,是因为延续 cont 被一个新延续捕获,而新延续又被另一个延续捕获,从而产生一种树状数据结构,如下所示:
(fun acc -> (f 1)::acc)
((fun acc -> (f 2)::acc)
((fun acc -> (f 3)::acc)
((fun acc -> (f 4)::acc)
((fun acc -> (f 5)::acc)
(id [])))))
它按顺序建立一个列表,而不需要你反转它。
不管它的价值如何,开始以非尾递归方式编写函数,它们更易于阅读和使用。
如果您有一个很大的列表要查看,请使用累加器变量。
如果您找不到以方便的方式使用累加器的方法,并且您没有任何其他选项可供使用,请使用延续。我个人认为不平凡、大量使用难以阅读的延续。