尾递归List.map

时间:2014-12-09 18:47:06

标签: list ocaml tail-recursion

OCaml中典型的List.map函数非常简单,它接受一个函数和一个列表,并递归地将该函数应用于列表的每个项目。我现在需要将List.map转换为尾递归函数,如何做到这一点?累加器应该积累什么?

2 个答案:

答案 0 :(得分:8)

可以说最简单的方法是根据遍历列表的尾递归辅助函数map实现map_aux,同时累积已经映射的前缀:

let map f l =
  let rec map_aux acc = function
    | [] -> acc
    | x :: xs -> map_aux (acc @ [f x]) xs
  in
  map_aux [] l

但是,由于list-catenation运算符@在其第一个参数的长度上需要线性时间,因此会产生二次时间遍历。此外,列表连接本身不是尾递归。

因此,我们希望避免使用@。此解决方案不使用列表连接,而是通过新映射的参数预先添加到累加器来累积:

let map f l =
  let rec map_aux acc = function
    | [] -> List.rev acc
    | x :: xs -> map_aux (f x :: acc) xs
  in
  map_aux [] l

要以正确的顺序恢复映射的元素,我们只需要在空列表的情况下反转累加器。请注意List.rev是尾递归的。

最后,这种方法通过累积所谓的difference list来避免递归列表 - 连接和列表反转:

let map f l =
  let rec map_aux acc = function
    | [] -> acc []
    | x :: xs -> map_aux (fun ys -> acc (f x :: ys)) xs
  in
  map_aux (fun ys -> ys) l

这个想法是让累积前缀列表由函数 acc表示,当应用于参数列表ys时,返回前缀为{的前缀列表。 {1}}。因此,作为累加器的初始值,我们有ys,表示空前缀,对于fun ys -> ys,我们只需将[]应用于acc即可获得映射前缀。

答案 1 :(得分:3)

(我会接受你说这不是家庭作业。)

答案是函数式编程中的经典模式之一,即cons / reverse习语。首先,你以相反的顺序排列列表,这很容易以尾递归的方式进行。最后,您反转列表。反转也是一种尾递归操作,因此不会对堆栈安全造成问题。

缺点是额外的分配和更笨拙的代码。

let map f list =
  let rec loop acc = function
    | [] -> List.rev acc
    | x::xs -> loop (f x::acc) xs in
  loop [] list

一个好的练习是(重新)实现一堆“标准”列表函数(appendrev_appendfold_leftfold_right,{{1} },filter等),使用这种风格使它们在必要时进行尾递归。