简体   繁体   中英

How to make an tail recursive function and test it? OCaml

I am a newbie on OCaml and I am learning tail recursive functions. I would like to make this functions recursive but I don't know where to start.

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);;

Is there any way to test if a function is recursive or not?

If you give a man a fish, you feed him for a day. But if you give him a fishing rod, you feed him for a lifetime.

Thus, instead of giving you the solution, I would better teach you how to solve it yourself.

A tail recursive function is a recursive function, where all recursive calls are in a tail position. A call position is called a tail position if it is the last call in a function, ie, if the result of a called function will become a result of a caller.

Let's take the following simple function as our working example:

let rec sum n = if n = 0 then 0 else n + sum (n-1)

It is not a tail recursive function as the call sum (n-1) is not in a tail position because its result is then incremented by one. It is not always easy to translate a general recursive function in a tail-recursive form. Sometimes, there is a tradeoff between efficiency, readability, and tail-recursion.

The general techniques are:

  1. use accumulator
  2. use continuation passing style

Using accumulator

Sometimes a function really needs to store the intermediate results, because the result of recursion must be combined in a non-trivial way. A recursive function gives us a free container to store arbitrary data called stack. Unfortunately, the stack container is bounded, and its size is unpredictable. So, sometimes, it is better to switch from the stack to the heap. The latter is slightly slower (because it introduces more work to the garbage collector), but is bigger and more controllable. In our case, we need only one word to store the running sum, so we have a clear win. We are using less space, and we're not introducing any memory garbage:

let sum n = 
  let rec loop n acc = if n = 0 then acc else loop (n-1) (acc+n) in
  loop n 0

However, as you may see, this came with a tradeoff - the implementation became slightly bigger and less understandable.

We used here a general pattern. Since we need to introduce an accumulator, we need an extra parameter. Since we don't want or can't change the interface of our function, we introduce a new helper function, that is recursive and will carry the extra parameter. The trick here is that we apply the summation before we do the recursive call, not after.

Using continuation passing style

It is not always the case when you can rewrite your recursive algorithm using an accumulator. In this case, a more general technique can be used - continuation passing style. Basically, it is close to the previous technique, but we will use a continuation in the place of an accumulator. A continuation is a function, that will actually postpone the work, that is needed to be done after the recursion, to the later time. Conventionally, we call this function return or simply k (for the continuation). Mentally, the continuation is a way of throwing the result of computation back into the future. "Back" is because you returning the result back to the caller, in the future, because, the result will be used not now, but once everything is ready. But let's look at the implementation:

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)

You may see, that we employed the same strategy, except that instead of int accumulator we used a function k as a second parameter. If the base case, if n is zero, we will return 0, (you can read k 0 as return 0 ). In the general case, we recurse in a tail position, with a regular decrement of the inductive variable n , however, we pack the work, that should be done with the result of the recursive function into a function: fun x -> k (x+n) . Basically, this function says, once x - the result of recursion call is ready, add it to the number n and return. (Again, if we will use name return instead of k it could be more readable: fun x -> return (x+n) ).

There is no magic here, we still have the same tradeoff, as with accumulator, as we create a new closure (functional object) at every recursive call. And each newly created closure contains a reference to the previous one (that was passed via the parameter). For example, fun x -> k (x+n) is a function, that captures two free variables, the value n and function k , that was the previous continuation. Basically, these continuations form a linked list, where each node bears a computation and all arguments except one. So, the computation is delayed until the last one is known.

Of course, for our simple example, there is no need to use CPS, since it will create unnecessary garbage and be much slower. This is only for demonstration. However, for more complex algorithms, in particular for those that combine results of two or more recursive calls in a non-trivial case, eg, folding over a graph data structure.

So now, armed with the new knowledge, I hope that you will be able to solve your problems as easy as pie.

Testing for the tail recursion

The tail call is a pretty well defined syntactic notion, so it should be pretty obvious whether the call is in a tail position or not. However, there are still few methods that allows one to check whether the call is in a tail position. In fact, there are other cases, when tail-call optimization may came into play. For example, a call that is right to the shortciruit logical operator is also a tail call. So, it is not always obvious when an call is using stack or it is a tail call. The new version of OCaml allows one to put an annotation at the call place, eg,

let rec sum n = if n = 0 then 0 else n + (sum [@tailcall]) (n-1)

If the call is not really a tail call, a warning is issued by a compiler:

Warning 51: expected tailcall

Another method is to compile with -annot option. The annotation file will contain an annotation for each call, for example, if we will put the above function into a file sum.ml and compile with ocaml -annot sum.ml , then we can open sum.annot file and look for all calls:

"sum.ml" 1 0 41 "sum.ml" 1 0 64
call(
  stack
)

If we, however, put our third implementation, then the see that all calls, are tail calls, eg grep call -A1 sum.annot :

call(
  tail
--
call(
  tail
--
call(
  tail
--
call(
  tail

Finally, you can just test your program with some big input, and see whether your program will fail with the stackoverflow. You can even reduce the size of the stack, this can be controlled with the environment variable OCAMLRUNPARAM , for example to limit the stack to one thousand of words:

export OCAMLRUNPARAM='l=1000'
ocaml sum.ml

You could do the following :

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;;

But for divide I prefer this solution :

 let divide l =
   let aux acc1 acc2 = function
     | [] -> acc1, acc2
     | hd::tl -> aux acc2 (hd :: acc1) tl
   in aux [] [] l;;

let merge ord (l1,l2) = 
  let rec aux acc l1 l2 = 
    match l1,l2 with
      | [],l | l,[] -> List.rev_append acc l
      | h1::t1,h2::t2 -> if ord h1 h2
        then aux (h1 :: acc) t1 l2
        else aux (h2 :: acc) l1 t2
  in aux [] l1 l2;;

As to your question about testing if a function is tail recursive or not, by looking out for it a bit you would have find it here .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM