Functional programming 使用continuation/CPS在OCaml中实现尾部递归合并排序

Functional programming 使用continuation/CPS在OCaml中实现尾部递归合并排序,functional-programming,ocaml,continuation-passing,Functional Programming,Ocaml,Continuation Passing,我试图在OCaml中实现一个尾部递归合并排序 因为Mergesort自然不是尾部递归,所以我使用CPS来实现它 我的实现也受到了 下面是我的代码 当我编译它,并使用1000000整数运行它时,它给出了堆栈溢出的错误为什么? 编辑 以下是我用于测试的代码: let compare_int x y = if x > y then 1 else if x = y then 0 else -1;; let create_list n = Random.self_init

我试图在
OCaml
中实现一个
尾部递归
合并排序

因为
Mergesort
自然不是尾部递归,所以我使用
CPS
来实现它

我的实现也受到了

下面是我的代码



当我编译它,并使用
1000000
整数运行它时,它给出了
堆栈溢出
的错误为什么?


编辑

以下是我用于测试的代码:

let compare_int x y =
  if x > y then 1
  else if x = y then 0
  else -1;;

let create_list n = 
  Random.self_init ();
  let rec create n' acc =
    if n' = 0 then acc
    else 
      create (n'-1) ((Random.int (n/2))::acc)
  in 
  create n [];;

let l = create_list 1000000;;

let sl = mergeSort_cps compare_int l;;
http://try.ocamlpro.com/
,它给出了以下错误:
异常:RangeError:超出了最大调用堆栈大小。


本地ocaml顶级
中,读取注释时没有任何问题

,您的
堆栈溢出
错误似乎很难重现

然而,您的代码并不完全处于CPS或尾部递归状态:在
merge\u sort
中,对
split\u list
merge
的调用是在非尾部调用位置进行的

问题是:通过进行CPS转换并大量使用累加器,与递归相关的最差堆栈深度是多少?在
sort
调用上保存堆栈深度实际上不是很有趣:当每个调用都将列表一分为二时,对于
n
输入列表的大小,最差的堆栈深度将是
O(logn)

相反,
split
merge
如果不是以累加器传递样式编写的,则会对堆栈进行线性
O(n)
使用,因此它们对tail-rec很重要。由于这些例程的实现是tail-rec,因此不必担心堆栈的使用,也不需要将排序例程本身转换为CPS形式,这会使代码更难阅读


(请注意,此对数递减参数特定于mergesort。在最坏的情况下,快速排序可以使用线性堆栈,因此将其设置为tail-rec可能很重要。)

添加另一个答案来说明另一个问题:回答者之间的许多困惑似乎是因为您不使用标准的OCaml编译器,而是使用Tryocam网站,该网站在javascript之上运行一个独特的OCaml后端,因此具有略微不同的优化和运行时特征

我可以可靠地再现这样一个事实:在上,您在长度
1\u 000\u 000
的列表上显示的CPS样式函数
mergeSort\u CPS
失败,错误如下:

Exception: InternalError: too much recursion.
我的分析是,这不是因为缺少tail-rec,而是因为Javascript后端缺乏对CPS转换调用tail-rec的非明显方式的支持:递归通过lambda抽象边界(但仍处于tail-position)

在直接、非尾部rec版本中转换代码可以消除问题:

let rec merge_sort compare = function
  | [] -> []
  | [hd] -> [hd]
  | l ->
    let (left, right) = split_list (List.length l / 2) l in
    merge compare (merge_sort compare left) (merge_sort compare right);;
正如我在另一个答案中所说,这段代码的堆栈深度是对数的,因此使用它不会产生堆栈溢出(tail rec不是一切)。Javascript后端可以更好地处理更简单的代码

请注意,通过使用更好的实现
split
(仍然使用您对
merge
的定义),您可以显著加快执行速度,从而避免重复遍历
列表。长度
然后拆分:

let split li =
  let rec split ls rs = function
    | [] -> (ls, rs)
    | x::xs -> split rs (x::ls) xs in
  split [] [] li;;

let rec merge_sort compare = function
  | [] -> []
  | [hd] -> [hd]
  | l ->
    let (left, right) = split l in
    merge compare (merge_sort compare left) (merge_sort compare right);;

我无法重现你的行为。当我在1000000个整数的列表上尝试您的代码时,它会对它们进行正确排序。它在顶级和编译代码中对我有效。使用默认堆栈设置的i386(32位)中的ocamlc和ocamlopt,尝试了10000000个整数的代码。没有堆栈溢出。仔细检查您的构建,如果它复制了,请澄清您的环境。@JeffreyScofield我尝试了我的代码,它给出了这样一个错误。我想这是该站点的一个bug。我不知道该站点的详细信息,但在许多虚拟机语言环境(JavaScript、Java等)中很难获得正确的尾部调用。很可能您使用的站点正在将OCaml翻译成JavaScript或类似的语言。如果您想深入了解OCaml,您可能应该使用INRIA版本。我在中尝试了我的代码,它给出了这样一个错误。我猜这是站点中的一个bug。我认为我的
split_list
merge
是尾部递归的。是的,但是它们在
sort
例程中用于非尾部位置,所以你仍然需要担心,比如说,是否递归调用
sort
(这里不是这种情况)。我如何转换整个代码,以便它们在
排序
例程中的尾部位置使用,因此我有
100%纯尾部递归
?@JacksonTale您确定这会解决您的问题吗?我怀疑你错了,这个bug在其他地方,例如在构建传递给排序函数的输入时。你能展示你测试的完整代码吗?你能进一步解释一下新的
split
功能吗?我真的不明白它是怎么工作的。@JacksonTale我建议你拿支笔和纸,用手执行
split[][1;2;3]
。好的,你的
split
版本不关心元素的顺序,也不关心
left
right
列表的顺序,但无论如何,它适合mergesort,因为mergesort只是合并两个列表,并不关心这些事情,对吗?如果我们需要一个<代码> SPLITYLISTRONU/COD>,只需在中间剪切一个列表,那么您的版本将不适合我知道<代码>列表。长度< /代码>将有<代码> O(n)< /代码>或只是<代码> o(1)< /代码>。
let split li =
  let rec split ls rs = function
    | [] -> (ls, rs)
    | x::xs -> split rs (x::ls) xs in
  split [] [] li;;

let rec merge_sort compare = function
  | [] -> []
  | [hd] -> [hd]
  | l ->
    let (left, right) = split l in
    merge compare (merge_sort compare left) (merge_sort compare right);;