10

My apologies to everyone for previous versions of this being vague. Someone has decided to have pity on the new girl and help me rewrite this question - here's an update that I hope will clear things up (and, thanks to all those who have been so generous with answers so far):


The Problem

I'm a new computer science student, in my first year at Uni. For the final project for my Algorithms class, we can pick any language we would like and implement a "refinement"/"efficiency" algorithm that is found natively (internally?) in another language, but missing in the language we pick.

We just recently studied recursion in class and my professor briefly mentioned that JavaScript does not implement Tail Recursion. From my research online, the new ECMA script 6 specification includes this feature, but it's not in any(/most?) JavaScript versions/engines at present? (sorry if I'm not sure which is which... I'm new at this).

My assignment is to provide 2 options for (coding a) WORK AROUND to the feature that is lacking.

So, my question is... does anyone, far more intelligent and experienced than I, have any thoughts or examples on how I might implement a:

WORK AROUND for the lack of Tail Recursion Optimization?

Community
  • 1
  • 1
  • 1
    How one would modify a JS engine to include TCO or how one would work around the lack of TCO? –  May 06 '14 at 15:34
  • @delnan Isn't this just a matter of changing your algorithm? –  May 06 '14 at 15:37
  • You could take a look at how Clojure (or more usefully ClojureScript) works around the issue. In particular, ClojureScript can compile the `recur` construct into javascript. But it's definitely a work-around, not an implementation of TCO. – rici May 06 '14 at 15:38
  • @Swasa At some level, yes. But especially for arbitrary tail calls (as opposed to simple tail recursions, which btw is what `recur` in Clojure is limited to), it's not always obvious how to change the algorithm. There are mechanical translations but they're rather complicated. –  May 06 '14 at 15:44
  • @delnan I know it's complicated... that's why I asked the experts :-) Don't you think it's worth asking... do you really think my question deserved to be downvoted? –  May 06 '14 at 15:46
  • Well, until a few minutes ago it was rather unclear, and even now it "does not show any research effort" (in the words of the downvote mouse-over). –  May 06 '14 at 15:48
  • You could write your programs in continuation passing style and call every function asynchronously. See the following thread: http://stackoverflow.com/q/14019341/783743 – Aadit M Shah May 06 '14 at 15:51
  • 1
    @Swasa - the onus on you is to provide us a good question. You'll find the crowd *is* indeed tough, but *very* helpful if you've done the legwork. – Jamiec May 06 '14 at 15:53
  • @Aadit and then pass the function to itself as an argument? What would that change except making everything more complicated? You would still have the unnecessary stack frames that you don't want, and they are even larger – Niklas B. May 06 '14 at 16:03
  • @NiklasB. I don't think you understood my comment. When you call a function asynchronously it is queued on the event loop and hence executes __after the function that called it returns__. This means that the call stack doesn't increase. In effect it is a hack for true tail call optimization in JavaScript. The continuation passing style ensures that you can still get return values from asynchronous functions. See my answer for more details: http://stackoverflow.com/a/23500518/783743 – Aadit M Shah May 06 '14 at 16:55
  • 1
    @AaditMShah I see, this is along the lines of georg's solution, just without the explicit interpreter (which I think is cleaner). Thanks for the link – Niklas B. May 06 '14 at 17:01
  • @NiklasB. BTW, Brian McKenna has an awesome tool for tail call optimization in JavaScript called [Brushtail](https://github.com/puffnfresh/brushtail). It's not perfect but it works in many cases. – Aadit M Shah May 07 '14 at 00:58
  • @delnan with ALL these comments and answers, u still think it's a bad question? –  May 07 '14 at 01:24
  • I hope this is an improvement everyone. Thanks so much for all your help so far!!! –  May 07 '14 at 10:27
  • I find the question much improved now. Reversed my vote and voted to reopen. – Niklas B. May 07 '14 at 13:00
  • I agree. @Swasa: Hang in there sista! –  May 11 '14 at 15:08

3 Answers3

16

One possible optimization of recursion is to evaluate lazily, that is, return a "computation" (=function) that will return a value instead of computing and returning it straight away.

Consider a function that sums up numbers (in a rather silly way):

function sum(n) {
    return n == 0 ? 0 : n + sum(n - 1)
}

If you call it with n = 100000 it will exceed the stack (at least, in my Chrome). To apply the said optimization, first convert it to true tail-recursive, so that the function returns just a call to itself and nothing more:

function sum(n, acc) {
    return n == 0 ? acc : sum(n - 1, acc + n)
}

and wrap this direct self-call with a "lazy" function:

function sum(n, acc) {
    return n == 0 ? acc : function() { return sum(n - 1, acc + n) }
}

Now, to obtain the result from this, we repeat the computation until it returns a non-function:

f = sum(100000, 0)
while(typeof f == "function")
    f = f()

This version has no problems with n = 100000, 1000000 etc

gog
  • 10,367
  • 2
  • 24
  • 38
6

As I mentioned in my comment, you could always convert your program into continuation passing style and then use asynchronous function calls to achieve true tail call optimization. To drive this point home consider the following example:

function foldl(f, a, xs) {
    if (xs.length === 0) return a;
    else return foldl(f, f(a, xs[0]), xs.slice(1));
}

Clearly this is a tail recursive function. So the first thing we need to do is convert it into continuation passing style which is really simple:

function foldl(f, a, xs, k) {
    if (xs.length === 0) k(a);
    else foldl(f, f(a, xs[0]), xs.slice(1), k);
}

That's it. Our function is now in continuation passing style. However there is still one big problem - there's no tail call optimization. This can however be easily solved using asynchronous functions:

function async(f, args) {
    setTimeout(function () {
        f.apply(null, args);
    }, 0);
}

Our tail call optimized foldl function can now be written as:

function foldl(f, a, xs, k) {
    if (xs.length === 0) k(a);
    else async(foldl, [f, f(a, xs[0]), xs.slice(1), k]);
}

Now all you need to do is use it. For example if you want to find the sum of the numbers of an array:

foldl(function (a, b) {
    return a + b;
}, 0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], function (sum) {
    alert(sum); // 55
});

Putting it all together:

function async(f, args) {
    setTimeout(function () {
        f.apply(null, args);
    }, 0);
}

function foldl(f, a, xs, k) {
    if (xs.length === 0) k(a);
    else async(foldl, [f, f(a, xs[0]), xs.slice(1), k]);
}

foldl(function (a, b) {
    return a + b;
}, 0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], function (sum) {
    alert(sum); // 55
});

Of course continuation passing style is a pain to write in JavaScript. Luckily there's a really nice language called LiveScript which adds the fun back into callbacks. The same functions written in LiveScript:

async = (f, args) ->
    setTimeout ->
        f.apply null, args
    , 0

foldl = (f, a, xs, k) ->
    if xs.length == 0 then k a
    else async foldl, [f, (f a, xs.0), (xs.slice 1), k]

do
    sum <- foldl (+), 0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    alert sum

Yes, it's a new language which compiles to JavaScript but it is worth learning. Especially since the backcalls (i.e. <-) allows you to write callbacks easily without nesting functions.

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
  • 1
    Wow, LiveScript is cool. Brings much more Haskell feeling to JS than CoffeeScript does – Niklas B. May 06 '14 at 17:24
  • Thanks again... you've been sooooo helpful! I've edited the question... hopefully it will be clearer to everyone. –  May 07 '14 at 10:28
0

Many of the commonest languages lack tail-recursion optimisation, because they simply don't expect you to use recursion to solve linear problems.

Tail-recursion optimisation only applies if a recursive call is the last thing your function does, meaning there's nothing that will need to look at the current stack content, and therefore no need to preserve it by adding another stack frame.

Any such algorithm can be adapted into an iterative form. For example (psuedocode):

 int factorial(int x) {
      return factTail(x,1);
 }

 int factTail(int x, int accum) {
      if(x == 0) {
          return accum;
      } else {
          return(factTail (x-1, x * accum);
      }
 }

... is an implementation of factorial() that has been tailored to ensure that the last statement is to return the outcome of a recursive call. An engine which knew about TCO would optimise this.

An iterative version that does things in the same order:

  int factorial(int x) {
      int accum = 1;
      for(int i=x; i>0; i--) {
          accum *= i;
      }
      return accum;
 }

(I made it count backwards to approximate the execution order of the recursive version -- in practice you probably wouldn't do this for factorial)

It's fine to use recursive calls if you know the recursion depth won't be huge (in this example, large values of x).

Often recursion leads to very elegant specifications of a solution. Fiddling with the algorithm to get a tail-call detracts from that. See how the factorial above is harder to understand than the classic:

 int factorial(int x) {
     if(x == 1) {
         return 1;
     } else {
         return factorial(x-1) * x;
     }
 }

... yet this classic form is stack-hungry, for a task that shouldn't need a stack. So it could be argued that an iterative form is the clearest way to solve this particular problem.

Due to the way programming is taught, most programmers today are more comfortable with the iterative forms than with recursive methods. Is there a particular recursive algorithm you're having specific trouble with?

slim
  • 40,215
  • 13
  • 94
  • 127
  • 1
    Another important use of tail calls is implementing state machines, which is (IMHO) a lot clearer implemented with tail calls than with `goto` (or it's cousin `switch`), particularly when data passes from state to state. I note in passing that tail-call optimization is *very* common in unix programming, although I suspect that many programmers don't recognize their use of it because they spell it something like `exec`. – rici May 06 '14 at 17:42
  • I hope this is an improvement everyone. Thanks so much for all your help so far!!! –  May 07 '14 at 10:27
  • 1
    @rici there's a ton of ways of doing state machines though, many of which don't involve recursion. `switch` is one; another is the OO way described in *Design Patterns*. – slim May 07 '14 at 10:59