Recursion 这个zip函数是递归的吗?

Recursion 这个zip函数是递归的吗?,recursion,ocaml,Recursion,Ocaml,我使用continuation实现了它。我认为这是尾部递归,但我听说不是。为什么它不是递归的 let rec zip_tr fc sc l1 l2 = match l1, l2 with | [], [] -> sc [] | [], _ -> fc (List.length l2) | _, [] -> fc (List.length l1) | h1::t1, h2::t2 -> zip_tr fc (fun l -> sc ((h1,

我使用continuation实现了它。我认为这是尾部递归,但我听说不是。为什么它不是递归的

let rec zip_tr fc sc l1 l2 = match l1, l2 with
  | [], [] -> sc []
  | [], _ -> fc (List.length l2)
  | _, [] -> fc (List.length l1)
  | h1::t1, h2::t2 -> 
    zip_tr fc (fun l -> sc ((h1, h2) :: l)) t1 t2

这条尾巴不是递归的吗?失败/成功的延续对尾部递归性有影响吗

代码中只有一个递归调用,它位于尾部位置。所以我想说你的函数是尾部递归的

它确实在
sc
参数中建立了一个相当大的计算量。但是,对
sc
的调用也处于尾部位置。在我的测试中,该函数适用于非常大的列表,而不会耗尽堆栈空间

如果我在一个很长的列表(100000000个元素)的两个副本上尝试您的函数,它将成功终止(经过相当长的时间)。这向我表明,它确实是尾部递归的

以下是包含长列表的会话:

# let rec zip_tr fc sc l1 l2 =  . . . ;;
val zip_tr :
  (int -> 'a) -> (('b * 'c) list -> 'a) -> 'b list ->
      'c list -> 'a = <fun>
# let rec mklong accum k =
      if k <= 0 then accum
      else mklong (k :: accum) (k - 1);;
val mklong : int list -> int -> int list = <fun>
# let long = mklong [] 100_000_000;;
val long : int list =
  [1; 2; 3; 4; 5; ...]
# let long_pairs =
    zip_tr (fun _ -> failwith "length mismatch")
           (fun x -> x) long long;;
val long_pairs : (int * int) list =
  [(1, 1); (2, 2); (3, 3); (4, 4); (5, 5); ...]
# List.length long_pairs;;
- : int = 100000000
它以相反的顺序生成结果,但对于长列表也会失败:

# zip_tr (fun _ -> failwith "length mismatch")
         (fun x -> x) [1;2] [3;4];;
- : (int * int) list = [(2, 4); (1, 3)]

# zip_tr (fun _ -> failwith "length mismatch")
         (fun x -> x) long long;;
Stack overflow during evaluation (looping recursion?).

我对OCaml代码生成的了解不足以详细解释这一点,但它确实表明您的代码确实是尾部递归的。然而,这可能取决于闭包的实现。对于不同的实现,为
sc
生成的计算可能会消耗大量堆栈。也许这就是你被告知的情况。

使用尾部递归函数,通过将每个
sc
包装到另一个匿名函数中,你可以构建一个类似于连续体链表的东西;然后,调用得到的延续

幸运的是,您的延续也是尾部递归的,因为对
sc
的一次调用的结果直接给出匿名闭包的结果。这就解释了为什么在测试时没有堆栈溢出

此函数的可能缺点是,它在开始执行任何实际工作之前分配了大量闭包(但仍然具有线性复杂性),而这不是通常所做的

这种方法的一个优点是,只有当已知两个列表的大小相同时,才会调用success continuation;更一般地说,在使用语言时,将代码编译为continuations是一件有趣的事情(这样您的努力就不会白费了)


如果函数是某个过程的一部分,您可能需要在遍历输入列表时以尾部递归的方式直接构建结果列表,而不会延迟后续工作。

您不能使用扩展名@tailcall进行检查吗?堆栈上没有分配闭包。它可以从函数返回或全局存储(转义它的作用域),因此它通常不能在堆栈上,即使有时可以。那里唯一的ocaml编译器从不使用堆栈,总是使用堆。这将使用大量堆。更传统的方法是反向构造列表,并将其作为参数传递,最后将其反向。这是一种用易变记录构建列表并最终将其变为列表的黑客方法。两者都有成本(易变性是昂贵的),并且分析显示两者的混合(反向构建块并将它们与易变性粘合在一起)性能最好。@GoswinvonBrederlow如果您愿意,我可以在答案中包含您的评论(有信用);出于好奇,你有没有参考过w.r.t.“易变性很昂贵”或“分析显示”的实验或文章?谢谢。我想评论是可以的。如果人们觉得评论有用的话,他们可以给它打分。至于参考资料,您必须在几年前的ocaml邮件列表中搜索关于iirc的讨论,即电池或核心中的tail recursive List.map。易变性代价高昂的原因是写入易变字段必须告诉GC。对于短列表(适合缓存),GC调用比构建和反转更昂贵。一旦超过缓存,则相反。
# zip_tr (fun _ -> failwith "length mismatch")
         (fun x -> x) [1;2] [3;4];;
- : (int * int) list = [(2, 4); (1, 3)]

# zip_tr (fun _ -> failwith "length mismatch")
         (fun x -> x) long long;;
Stack overflow during evaluation (looping recursion?).