Tail Recursion optimization for JavaScript?

后端 未结 3 1223
生来不讨喜
生来不讨喜 2021-02-03 11:45

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

相关标签:
3条回答
  • 2021-02-03 11:46

    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

    0 讨论(0)
  • 2021-02-03 12:01

    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?

    0 讨论(0)
  • 2021-02-03 12:03

    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.

    0 讨论(0)
提交回复
热议问题