Reputation: 5
(* val rev : ’a list -> ’a list -> ’a list *)
let rec rev l r =
match l with
[] -> r
| (h::t) -> rev t (h::r)
Could some one please tell me what happens here recursively? Also, why are two arguments l and r used in the code?
Let's say I want to reverse [1;2;3] how does the function reverse is to 3,2,1?
rev [1;2;3] []
Upvotes: 0
Views: 803
Reputation: 8390
What's with the r
Going into detail, let's start with a function without r
(accumulator):
let rec rev l =
match l with
[] -> l
| h :: t -> rev t @ [h]
What is inefficient here is not just because it's not tail-recursive (which I won't cover in detail; see the link below), but also because you have to append an element [h]
to the end of every reversed sublist on every call. In Ocaml, appending to a list is inefficient compared to prepending to it, because a list is a just a singly-linked list, which you only have access to the head's pointer.
Prepending a new element to the head of the list requires only a newly created element to create a pointer to the previous head element and returning a newly created list with that new element as the head. This is only an O(1) operation.
Appending an element to the list, however, incurs an O(N) complexity because you have to traverse to the end of the list before creating a new pointer for the last element to point to the new element. If you have a list of length N
where N
is a huge number, O(N) is pretty bad.
With an accumulator (r
), you are "accumulating" or modifying a state and passing into the next function call. This is the basis of tail-recursion but also avoid the above pitfall. To see what it really does, see the pseudocode below.
What the recursive function does
Here is a visual representation of what happens as the recursive operation goes on (first argument being l
and second being r
).
(* pseudocode *)
(* while List.length l <> 0 *)
rev [1; 2; 3; 4] []
rev [2; 3; 4] (1 :: [])
rev [3; 4] (2 :: [1])
rev [4] (3 :: [2; 1])
rev [] (4 :: [3; 2; 1])
(* List.length l = 0 *)
return [4; 3; 2; 1]
In practice, you might want to create an inner "helper" function that takes care of the recursive heavy-lifting and leave the outer function simply as a user-friendly, single-arity API.
let rev l =
let rec helper l' r =
match l' with
[] -> r
| (h::t) -> helper t (h::r)
in helper l []
this article on recursion I wrote might help you understand tail-recursion better.
Upvotes: 2
Reputation: 1965
This function is recursive because there is a call of rev
inside itself. r
parameter is here so called accumulator - thanks to it this function is tail recursive.
How does it work in details?
rev [1;2;3] []
, so l=[1;2;3]
and r=[]
. L is matched with h::t
- that means head and tail of the list. h=1
, t=[2;3]
. rev
function is rev [2;3] [1]
l
is matched as previous, so rev
is called with [3] [2;1]
rev [] [3;2;1]
. L
is matched with []
and r
(=[3;2;1]
) is returned. Note that you can hide accumulator in the following way:
let rev l =
let rec rev l r = match l with
[] -> r
| (h::t) -> rev t (h::r)
in
rev l []
Then you can use this function passing only one argument rev [1;2;3]
.
Upvotes: 1