【问题标题】:Why does the OCaml std lib have so many non-tail-recursive functions?为什么 OCaml 标准库有这么多非尾递归函数?
【发布时间】:2012-08-17 20:12:32
【问题描述】:

我最近一直在将许多 OCaml 标准库函数重写为尾递归。鉴于这需要直接的 CPS 转换,我很困惑为什么默认版本不是这样编写的。

例如,在标准库中,map被定义为:

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

我已经改写成:

let map f l =
  let rec aux l k = match l with
      []   -> k []
    | a::l -> aux l (fun rest -> k (f a :: rest))
  in aux l (fun x -> x)

【问题讨论】:

  • Isint CPS 版本的功能是否比尾递归版本更高效?
  • @Gab CPS 版本的递归函数必然是尾递归的。
  • map函数是tail recursion modulo cons的一个例子。

标签: ocaml tail-recursion continuation-passing


【解决方案1】:

根据我的经验,非平凡函数的尾递归版本通常会在空间效率与时间效率之间进行权衡。换句话说,标准库中的函数对于较小的输入可能会更快。

【讨论】:

  • 如何做出这种权衡?我可以想象在没有消除尾调用的递归函数中推送和弹出堆栈帧的成本会导致空间和时间效率降低。不是这样吗?
  • 嗯,列表函数的尾递归版本通常会反向构建结果,然后在最后反转它。对于短输入,非尾递归版本避免了反转。广义延续传递可以创建很多闭包(或者我可以想象)。
【解决方案2】:

好吧,您的代码正在堆中构建并传递闭包的“链表”(每个闭包都将前一个闭包捕获为k),而不是调用堆栈上的帧堆栈。

一种更常见、等效的尾递归方式是传递到目前为止的结果列表(相反,因为您只能有效地添加到前面),然后在最后反转它:

let map f l =
  let rec aux l acc = match l with
      []   -> List.rev acc
    | a::l -> aux l (f a :: l)
  in aux l

(这个和List.rev (List.rev_map f l)基本一样)

在这种情况下,累积的是迄今为止的结果列表(反向),而不是闭包。但效果完全一样。

在这两种情况下,我们都需要线性空间来存储某种中间表示(除了输入列表和输出列表之外),因此就内存使用的复杂性而言,与非尾递归版本相比没有优势。虽然堆上的内存确实比堆栈上的内存多,但使用尾递归版本可能比非尾递归版本适用于更大的列表。就绝对内存使用而言,累积列表选项可能是最有效的,因为闭包和堆栈帧都有更多的开销。

【讨论】:

    猜你喜欢
    • 2014-04-05
    • 2013-02-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多