1

Hi i have some difficulty in understanding tail-recursivity. I know thats it's important to avoid infinite loops and also for memory usage. I've seen some examples on simple functions like Fibonacci in "Expert in F#", but I don't think i've seen code when the result is something different than just a number.

What would be the accumulator then ? i'm not sure...

Here is a recursive function that I've written. It counts the number of inversions in an array, using the quicksort algorithm. [it's taken from an exercise of the Coursera MOOC Algo I by Stanford]

I'd be grateful if somebody could explain how to make that tail recursive. [Also, i've translated that code from imperative code, as i had written that in R before, so the style is not functional at all...]

another question: is the syntax correct, A being a (mutable) array, i've written let A = .... everywhere ? is A <- .... better / the same ?

open System.IO
open System


let X = [|57; 97; 17; 31; 54; 98; 87; 27; 89; 81; 18; 70; 3; 34; 63; 100; 46; 30; 99;
    10; 33; 65; 96; 38; 48; 80; 95; 6; 16; 19; 56; 61; 1; 47; 12; 73; 49; 41;
    37; 40; 59; 67; 93; 26; 75; 44; 58; 66; 8; 55; 94; 74; 83; 7; 15; 86; 42;
    50; 5; 22; 90; 13; 69; 53; 43; 24; 92; 51; 23; 39; 78; 85; 4; 25; 52; 36;
    60; 68; 9; 64; 79; 14; 45; 2; 77; 84; 11; 71; 35; 72; 28; 76; 82; 88; 32;
    21; 20; 91; 62; 29|]

// not tail recursive. answer = 488

let N = X.Length

let mutable count = 0

let swap (A:int[]) a b =
    let tmp = A.[a]
    A.[a] <- A.[b]
    A.[b] <- tmp
    A

let rec quicksortNT (A:int[]) = 
    let L = A.Length


    match L with 
         | 1 -> A
         | 2 -> count <- count + 1
                if (A.[0]<A.[1]) then A 
                   else [|A.[1];A.[0]|]

         | x -> let p = x
                let pval = A.[p-1]
                let A = swap A 0 (p-1)
                let mutable i = 1
                for j in 1 .. (x-1) do 
                     if (A.[j]<pval) then let A = swap A i j
                                          i <- i+1
                // end of for loop

                // putting back pivot at its right place
                let A = swap A 0 (i-1)
                let l1 = i-1
                let l2 = x-i

                if (l1=0) then
                            let A =  Array.append [|A.[0]|] (quicksortNT A.[1..p-1])               
                            count <- count + (l2-1)
                            A
                     elif (l2=0) then 
                            let A = Array.append (quicksortNT A.[0..p-2]) [|A.[p-1]|]
                            count <- count + (l2-1)
                            A
                else
                            let A = Array.append ( Array.append (quicksortNT A.[0..(i-2)]) [|A.[i-1]|] ) (quicksortNT A.[i..p-1])
                            count <- count + (l1-1)+(l2-1)
                            A


let Y = quicksortNT X
for i in 1..N do printfn "%d" Y.[i-1]
printfn "count = %d" count

Console.ReadKey() |> ignore

Thank you very much for your help

Will Ness
  • 70,110
  • 9
  • 98
  • 181
Fagui Curtain
  • 1,867
  • 2
  • 19
  • 34
  • if you swap inplace like you do here it makes IMO absolutely no sense to go away from the imperative solution using loops – Random Dev Jan 18 '16 at 09:10
  • I hate to say it but I think the example you picked to use for learning recursion and accumulators is too hard as a first example. The reason I say this is because while Carsten gave a great and detailed answer, to do it with the example you gave he had to use a continuation which is not an easy to grasp concept; when combined with trying to learn recursion it makes it that much harder. Carsten did note that he could do it with an accumulator. While you have a good question, you should allow the answerer to offer a simpler option that has an accumulator of a value instead of a function. – Guy Coder Jan 18 '16 at 13:35
  • 1
    Take a look at [How can I implement a tail-recursive list append](http://stackoverflow.com/a/2867646/1243762) for a more basic example. Notice how complicated the continuation version is compared to the other two. I am not saying to never use continuations, only that you need to understand them before using them. – Guy Coder Jan 18 '16 at 14:00

2 Answers2

5

As I said in my comment: you do inplace-swapping so it makes no sense to recreate and return arrays.

But as you ask about tail-recursive solutions look at this version using lists and continuation-passing-style to make the algorithm tail-recursive:

let quicksort values =
    let rec qsort xs cont =
        match xs with
        | [] -> cont xs
        | (x::xs) ->
            let lower = List.filter (fun y -> y <= x) xs
            let upper = List.filter (fun y -> y > x) xs
            qsort lower (fun lowerSorted ->
                qsort upper (fun upperSorted -> cont (lowerSorted @ x :: upperSorted)))
    qsort values id

remarks:

  • you can think of it like this:
    • first partition the input into upper and lower parts
    • then start with sorting (recursively) the lower part, when you are done with this continue by...
    • ... take lowerSorted and sort the upper part as well and continue with ...
    • ... take both sorted parts, join them and pass them to the outer continuation
    • the outermost continuation should of course just be the id function
  • some will argue that this is not quicksort as it does not sort inplace!
  • maybe it's hard to see but it's tail-recursive as the very last call is to qsort and it's result will be the result of the current call
  • I used List because the pattern-matching is so much nicer - but you can adopt this to your version with arrays as well
  • in those cases (as here) where you have multiple recursive calls I always find cont-passing solutions to be easier to write and more natural - but accumulators could be used as well (but it will get messy as you need to pass where you are too)
  • this will not take less memory than the version without the cont-passing at all - it just will be placed on the heap instead of the stack (you usually have way more heap available ;) ) - so it's a bit like cheating
  • that's why the imperative algorithm is still way better performance-wise - so a usual compromise is to (for example) copy the array, use the inplace-algorithm on the copy and then return the copy - this way the algorithm behaves as if it's pure on the outside
Random Dev
  • 51,810
  • 9
  • 92
  • 119
  • if i sticked to my code, instead of making new arrays, id like just modifying the current array. is it what my code is doing or not ? or am i creating new instances each time i write let A ? is my syntax correct ? – Fagui Curtain Jan 18 '16 at 09:58
  • of course you are creating new ones (`else [|A.[1];A.[0]|]` and the `Array.append` calls) - but at the same time you mutate the original one with `let A = swap A 0 (p-1)` etc. (so the original will be totally destroyed as you can test yourself) - I would recommend that you don't return the input if you mutated it - both in `swap` as well as in your `quicksortNT` - this way it's obvious that your function is *impure* and has side-effects plus it forces you to think your algorithm through – Random Dev Jan 18 '16 at 10:04
  • you can just translate the imperative algorithm into F# - it's all there - mutation with `<-`, `for` loops, etc. - no need to use recursion at all in this case - if on the other side you want to write functional/pure code you should think about something in the direction of my answer - it all has positive and negative consequences and the *best* solution depends on your goals – Random Dev Jan 18 '16 at 10:06
  • yes i could see in the debugger the original array is destroyed. if thats impurity. a way to avoid that I think of would be to create the recursive function INSIDE the function being called, and if A is the argument which is non mutable (list), create a mutable B with let B = List.toArray and then call the recursive function. Oops thats what you wrote ! – Fagui Curtain Jan 18 '16 at 10:11
  • the fear i have is that with doing let A = ... i would be increasing memory usage, but I'm not, am I ? a mutable is just like a pointer ? – Fagui Curtain Jan 18 '16 at 10:12
  • yes in .NET world you just copy the refrences around with `let A = ...` where you point to arrays – Random Dev Jan 18 '16 at 10:14
  • there is one more subtlety if you can clarify it. here we are looking to compute inversions. so how to pass that in the recursion ? as an accumulator ? we have 2 variables to recurse on: the array AND the current count of inversions. – Fagui Curtain Jan 18 '16 at 15:20
  • 2
    you can just add it as an result (you have to adapt the continuation too) - just tuple it up with the sorted list/array – Random Dev Jan 18 '16 at 15:56
1

The whole point to quicksort's swapping partition procedure is that it can mutate the same array; you just pass it the low and the high index of the array's range it has to process.

So make a nested function and pass it just the 2 indices. To make it tail recursive, add the third parameter, list-of-ranges-to-process; when that becomes empty, you're done. Wikibook says you mutate arrays with A.[i] <- A.[j].

A nested function can access its parent function's argument directly, because it is in scope. So, make swap nested too:

let rec quicksort (A:int[]) = 

    let swap a b =
        let tmp = A.[a]
        A.[a] <- A.[b]
        A.[b] <- tmp

    let todo =  ... (* empty list *)

    let rec partition low high = 
       .... (* run the swapping loop, 
               find the two new pairs of indices,
               put one into TODO and call *)
       partition new_low new_high

    let L = A.Length

    match L with 
     | 1 -> (* do nothing   A *)
     | 2 -> count <- count + 1
            if (A.[0]<A.[1]) then (* do nothing   A *)
               else (* [|A.[1];A.[0]|] *) swap 1 0

     | x -> ....
            partition 0 L

So partition will be tail recursive, working inside the environment set up for it by quicksort.

(disclaimer: I don't know F# and have never used it, but I know Haskell and Scheme, to some degree).

Will Ness
  • 70,110
  • 9
  • 98
  • 181