Recursion 如何制作尾部递归函数并对其进行测试?奥卡姆

Recursion 如何制作尾部递归函数并对其进行测试?奥卡姆,recursion,ocaml,tail-recursion,Recursion,Ocaml,Tail Recursion,我是OCaml的新手,正在学习尾部递归函数。我想让这个函数递归,但我不知道从哪里开始 let rec rlist r n = if n < 1 then [] else Random.int r :: rlist r (n-1);; let rec divide = function h1::h2::t -> let t1,t2 = divide t in h1::t1, h2::t2 | l -> l,[];; let re

我是OCaml的新手,正在学习尾部递归函数。我想让这个函数递归,但我不知道从哪里开始

let rec rlist r n =
    if n < 1 then []
    else Random.int r :: rlist r (n-1);;

let rec divide = function
    h1::h2::t -> let t1,t2 = divide t in
        h1::t1, h2::t2
    | l -> l,[];;

let rec merge ord (l1,l2) = match l1,l2 with
    [],l | l,[] -> l
    | h1::t1,h2::t2 -> if ord h1 h2
        then h1::merge ord (t1,l2)
        else h2::merge ord (l1,t2);;
让rec rlist r n=
如果n<1,则[]
else Random.int r::rlist r(n-1);;
设rec divide=函数
h1::h2::t->让t1,t2=除以t
h1::t1,h2::t2
|l->l,[];;
让rec合并ord(l1,l2)=将l1,l2与
[],l | l,[]->l
|h1::t1,h2::t2->if ord h1 h2
然后h1::合并ord(t1,l2)
else h2::合并ord(l1,t2);;

有没有办法测试函数是否为递归函数?

您可以执行以下操作:

let rlist r n =
  let aux acc n =
    if n < 1 then acc
    else aux (Random.int r :: acc) (n-1)
  in aux [] n;;

let divide l =
  let aux acc1 acc2 = function
    | h1::h2::t -> 
        aux (h1::acc1) (h2::acc2) t
    | [e] -> e::acc1, acc2
    | [] -> acc1, acc2
  in aux [] [] l;;
至于你关于测试一个函数是否是尾部递归的问题,通过仔细查找,你会发现它

如果你给一个人一条鱼,你就喂他一天。但如果你给他一根鱼竿,你就可以养活他一辈子

因此,与其给你答案,不如我自己教你如何解决

尾部递归函数是一个递归函数,其中所有递归调用都位于尾部位置。如果调用位置是函数中的最后一个调用,即如果调用函数的结果将成为调用方的结果,则调用位置称为尾部位置

让我们以以下简单函数为例:

let rec sum n = if n = 0 then 0 else n + sum (n-1)
它不是尾部递归函数,因为调用
sum(n-1)
不在尾部位置,因为它的结果随后递增1。将一般递归函数转换为尾部递归形式并不总是容易的。有时,在效率、可读性和尾部递归之间需要权衡

一般技术包括:

  • 使用蓄能器
  • 使用连续传球方式
  • 使用蓄能器 有时函数确实需要存储中间结果,因为递归的结果必须以非平凡的方式组合。递归函数为我们提供了一个自由容器来存储名为stack的任意数据。不幸的是,堆栈容器是有界的,其大小是不可预测的。因此,有时候,最好从堆栈切换到堆。后者稍微慢一点(因为它给垃圾收集器带来了更多的工作),但更大、更可控。在我们的例子中,我们只需要一个字来存储运行的总和,所以我们有一个明显的胜利。我们使用的空间更少,并且没有引入任何内存垃圾:

    let sum n = 
      let rec loop n acc = if n = 0 then acc else loop (n-1) (acc+n) in
      loop n 0
    
    然而,正如您所看到的,这是一个折衷方案——实现变得稍微大一些,不易理解

    我们在这里使用了一种通用模式。因为我们需要引入累加器,所以我们需要一个额外的参数。由于我们不想或不能更改函数的接口,我们引入了一个新的助手函数,它是递归的,并将携带额外的参数。这里的技巧是,我们在进行递归调用之前应用求和,而不是之后

    使用连续传球方式 使用累加器重写递归算法并不总是如此。在这种情况下,可以使用一种更通用的技术-连续传球方式。基本上,它与前面的技术很接近,但我们将使用一个continuation来代替累加器。延拓是一个函数,它实际上会将递归后需要完成的工作推迟到以后。通常,我们将此函数称为
    return
    或简称为
    k
    (用于延续)。从精神上讲,延续是将计算结果抛回未来的一种方式。“返回”是因为您在将来将结果返回给调用者,因为结果不是现在使用,而是在一切就绪后使用。但让我们看一下实现:

    let sum n = 
      let rec loop n k = if n = 0 then k 0 else loop (n-1) (fun x -> k (x+n)) in
      loop n (fun x -> x)
    
    您可能会看到,我们采用了相同的策略,除了使用函数
    k
    作为第二个参数而不是
    int
    累加器之外。如果基本情况是,如果
    n
    为零,我们将返回0,(您可以将
    k0
    读取为
    返回0
    )。在一般情况下,我们在尾部位置递归,有规律地减少归纳变量
    n
    ,然而,我们将递归函数的结果所做的工作打包成一个函数:
    fun x->k(x+n)
    。基本上,该函数表示,一旦递归调用的结果准备就绪,将其添加到编号
    n
    ,然后返回。(同样,如果我们使用name
    return
    而不是
    k
    ,它可能更可读:
    funx->return(x+n)

    这里没有魔法,我们仍然有相同的折衷,就像累加器一样,在每次递归调用时创建一个新的闭包(函数对象)。每个新创建的闭包都包含对前一个闭包(通过参数传递)的引用。例如,
    funx->k(x+n)
    是一个函数,它捕获两个自由变量,值
    n
    和函数
    k
    ,这是前面的延续。基本上,这些连续体形成一个链表,其中每个节点都有一个计算和除一个之外的所有参数。因此,计算延迟到最后一个已知

    当然,对于我们的简单示例,没有必要使用CPS,因为它将创建不必要的垃圾,并且速度会慢得多。这只是为了演示。然而,对于更复杂的算法,特别是对于那些在非平凡情况下组合两个或多个递归调用结果的算法,例如,折叠图形数据结构

    所以,现在,有了新的知识,我希望你们能轻而易举地解决你们的问题

    尾部递归的测试 tail调用是一个定义非常好的语法概念,因此无论调用是否处于tail位置,都应该非常明显。然而,仍然有一些方法允许人们检查呼叫是否在尾部pos中
    let sum n = 
      let rec loop n k = if n = 0 then k 0 else loop (n-1) (fun x -> k (x+n)) in
      loop n (fun x -> x)
    
    let rec sum n = if n = 0 then 0 else n + (sum [@tailcall]) (n-1)
    
    Warning 51: expected tailcall
    
    "sum.ml" 1 0 41 "sum.ml" 1 0 64
    call(
      stack
    )
    
    call(
      tail
    --
    call(
      tail
    --
    call(
      tail
    --
    call(
      tail
    
    export OCAMLRUNPARAM='l=1000'
    ocaml sum.ml