I'm attempting problem 8 of the 99 OCaml problems, which asks you to write a function compress that removes consecutive duplicate entires in a list:
assert (
compress
[ "a"; "a"; "a"; "a"; "b"; "c"; "c"; "a"; "a"; "d"; "e"; "e"; "e"; "e" ]
= [ "a"; "b"; "c"; "a"; "d"; "e" ])
I came to the following solution:
let head = function x :: _ -> Some x | [] -> None
let compress list =
let rec fn old_list new_list =
match (old_list, new_list) with
| h :: t, _ ->
fn t (if Some h = head new_list then new_list else h :: new_list)
| _ -> new_list
in
List.rev (fn list [])
;;
The provided example solution by the website is as follows:
let rec compress = function
| a :: (b :: _ as t) -> if a = b then compress t else a :: compress t
| smaller -> smaller;;
At first, I thought that my solution was more efficient as it is tail recursive, while the provided solution is clearly not (requires us to keep the a in a :: compress t on the stack). However, when I test if my code is tail recursive:
assert (
(compress [@tailcall])
[ "a"; "a"; "a"; "a"; "b"; "c"; "c"; "a"; "a"; "d"; "e"; "e"; "e"; "e" ]
= [ "a"; "b"; "c"; "a"; "d"; "e" ])
It gives me a warning saying it's not tail recursive. Why?
From my understanding, my solution doesn't require keeping any state on the stack, which should make it tail recursive.
EDIT
Also tried applying the [@tailcall] to fn directly via List.rev ((fn [@tailcall]) list []), get the same warning.
When you make your assertion,
compressisn't in tail position,(=)is. This has no bearing on yourcompressfunction implementation.Similarly, in the expression
List.rev ((fn [@tailcall]) list []), the call offnis not in the tail call position.You can test this by trying:
Note also that tail-recursive does not always mean more efficient. Tail-recursive functions are often less efficient, but they can be used on large data without a stack overflow. If you are dealing with data likely to cause this, it suggests you may need to re-evaluate the data structure you're using.
We could also, as of OCaml 4.14 make
compresstail-recursive withtail_mod_cons.Alternatively, you might implement this with continuation passing.
As yet another fun alternative that is tail-recursive, you might write a
compressfunction which generates a sequence. In order to do this, ourauxfunction needs to take an argument to keep track of the last value seen. Because at the beginning there will not be a last value seen, theoptiontype makes sense.Of course, it may also be helpful to have this work directly on a sequence as the input, allowing it to work on other data types without first converting to a list. The translation is very straightforward.