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?

16
  • 1
    How one would modify a JS engine to include TCO or how one would work around the lack of TCO? Commented May 6, 2014 at 15:34
  • @delnan Isn't this just a matter of changing your algorithm? Commented May 6, 2014 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. Commented May 6, 2014 at 15:38
  • 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. Commented May 6, 2014 at 15:53
  • 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 Commented May 6, 2014 at 17:01

3 Answers 3

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

Sign up to request clarification or add additional context in comments.

5 Comments

This is cool. It does not only prevent a Stack Overflow, it actually uses only O(1) space if I'm not mistaken.
@NiklasB.: correct. It doesn't consume stack space at all.
@NiklasB. allocating 1000000 function objects doesn't seem O(1) to me
@Esailija O(1) space complexity means a constant amount of space occupied at any given point in time (modulo GC non-determinism)
Just to add to this great explanation, this is what is called a Trampoline in javascript
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.

2 Comments

Wow, LiveScript is cool. Brings much more Haskell feeling to JS than CoffeeScript does
Thanks again... you've been sooooo helpful! I've edited the question... hopefully it will be clearer to everyone.
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?

3 Comments

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.
I hope this is an improvement everyone. Thanks so much for all your help so far!!!
@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.