使用stdlib List.map“评估期间堆栈溢出”

时间:2016-06-23 14:29:07

标签: functional-programming ocaml tail-recursion

说我有一堆:

Array.make (int_of_float (2. ** 17.)) 1
    |> Array.to_list;;
- : int list
= [1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1;
   1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1;
   1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1;
   1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1;
   1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1;
   1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1;
   1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1;
   1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1;
   1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1;
   1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1;
   1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1;
   1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; ...]

我想在这些函数上映射一个函数:

Array.make (int_of_float (2. ** 17.)) 1
    |> Array.to_list
    |> List.map (fun x -> x * 2);;
Stack overflow during evaluation (looping recursion?). 

好像太多了!看看如何在Ocaml中实现List.map,我发现了这一点 (https://github.com/ocaml/ocaml/blob/trunk/stdlib/list.ml#L57-L59):

let rec map f = function
    [] -> []
  | a::l -> let r = f a in r :: map f l

我对Ocaml很陌生,但看起来map的编写方式使得它不是尾递归的。

是吗?

如何将功能映射到很多东西上?

3 个答案:

答案 0 :(得分:4)

是的,OCaml标准库中的List.map不是尾递归的。你有几个选择:

  1. 使用Array.map
  2. 使用List.rev_map(可能与List.rev创造)
  3. 使用一些更适合您任务的其他数据结构
  4. 不要使用标准库
  5. 虽然前两个选项很明显,但后两个选项需要一些解释。

    更好的数据结构

    如果您确实拥有大量数据,并且此数据是数字的,那么您应该考虑使用Bigarrays。此外,根据您的使用情况,Maps和Hashtables可能会更好。函数式编程语言中的人倾向于在任何地方使用列表,而不是选择适当的数据结构。不要陷入这个陷阱。

    其他库及其为非尾递归的原因

    List.map非尾递归是有充分理由的。堆栈使用和性能之间总是存在权衡。对于小列表(这是最常见的用例),非尾递归版本要快得多。因此标准库决定提供快速List.map,如果您需要处理大型列表,则可以使用List.rev_map xs |> List.rev。此外,有时您可以省略List.rev部分。所以,标准库,不是试图为你思考,它给你一个选择。

    另一方面,随着时间的推移,人们设法在这种权衡中找到一些最优,即具有相当快的堆栈版本。解决方案是在列表很小时使用非尾递归方法,然后回退到慢速版本。此类实现由Janestreet Core LibraryBatteriesContainers库提供。

    因此,您可以切换到这些库并忘记此问题。虽然仍然强烈建议使用List.map几乎不小心,因为此操作总是具有线性内存消耗(堆或堆栈内存),这是可以避免的。因此,最好尽可能使用rev_map

答案 1 :(得分:3)

你是正确的List.map不是尾递归的,而是使用List.rev_map而不是尾递归。 ...和List.rev应该是尾递归。

List记录了List模块的所有功能,指示它是否不是尾递归。

答案 2 :(得分:2)

您要做的是在计算结束时构建列表:

let construct_value _ =
 1 |> (fun x -> x * 2)

let rec construct_list f n =
 let rec aux acc n =
  if n < 0
  then acc
  else aux (f n) (n-1)
 in
 aux [] (n-1)

let result = construct_list construct_value (int_of_float (2. ** 17.))

这里最终会得到与计算相同的列表,但时间和内存使用量会减少。请注意,列表的元素是从列表的底部构造的(你必须为尾部递归做),我添加了一个“index”参数,它对应于数组中的确切位置(我没有构造,另一个时间和记忆增益)

另外,如果你想对那么大的数据结构进行计算,列表可能不是最好的选择,但这取决于你打算用它做什么。