105

I have been trying to understand Tail call optimization in context of JavaScript and have written the below recursive and tail-recursive methods for factorial().

Recursive:

function factorial (n) {
  if (n < 2) {
    return 1;
  } else {
    return n * factorial(n-1);
  }
}

Tail-recursive:

function factorial (n) {
  function fact(n, acc) {
    if (n < 2) {
      return acc;
    } else {
      return fact(n-1, n * acc);
    }
  }

  return fact(n, 1)
}

But I am not sure if the tail-recursive version of the function will be optimised by JavaScript compiler as it is done in other languages like Scala etc. Can someone help me out on this one?

Aditya Singh
  • 15,810
  • 15
  • 45
  • 67

5 Answers5

99

Update: As of July 22, 2023 Safari is the only browser that supports tail call optimization.

If the goal is to simply work around the stack limit, as other answers have shown, it is possible. All that is required is a lazy function and a trampoline. This is not the same as tail call optimization.

const trampoline = (x) => {
    while (typeof x == 'function') x = x()
    return x;
}

const lazy = (f) => (...args) => () => f(...args)

function factorial (n) {
  const f = lazy((a, n) => n == 0 ? a : f(n * a, n - 1));
  return trampoline(f(1, n));
}

console.log(factorial(30000)); // Infinity

The chromium team explicitly states that Tail Call Optimization is not under active development and can be tracked here.

The implementation for Firefox can be tracked here

Original Post

Yes, ES2015 offers tail call optimization in strict mode. Dr. Axel Rauschmayer lays it out beautifully at the link below so I shall not repeat his words here.

Note: ES 5 does not optimize tail calls.

http://www.2ality.com/2015/06/tail-call-optimization.html

sheeldotme
  • 2,237
  • 14
  • 27
  • Important caveat: it can only be optimized in strict mode. – Barmar May 14 '16 at 09:32
  • 22
    Having tail call optimization in ES2015 is distinctly different from all or even most implementations which otherwise claim to be ES2015-compliant doing proper tail call optimization. The correct answer to the question of "Are functions in JavaScript tail-call optimized?" is "they are if the engine does, otherwise they aren't." See https://www.chromestatus.com/feature/5516876633341952. –  May 14 '16 at 10:30
  • 5
    I'm pretty sure that browsers will also tail-call-optimise ES5 code when they implement it for ES6. It's just that the ES6 specification now explicitly requires it (and still, no engine has yet implemented it). – Bergi May 14 '16 at 10:56
  • 1
    The 'ECMAScript compatibility table' is a good resource to check the compatibility. https://kangax.github.io/compat-table/es6/ – Eldho NewAge May 24 '18 at 13:22
  • The thing that annoys me the most about browsers not implementing TCO is that reading the source code of the popular frameworks, most of them could benefit enormously from TCO performance-wise if they could rely on it being available. Every virtual DOM implementation tends to be written in continuation passing style, and currently has to use a trampoline. – saolof Jan 20 '23 at 18:34
29

As the other answers have said, not in practice. However, you can define a utility to help out.

class Tco {
  constructor(func) {
    this.func = func;
  }
  execute() {
    let value = this;
    while (value instanceof Tco)
      value = value.func();
    return value;
  }
}

const tco = (f) => new Tco(f);
function factorial (n) {
  const fact = (n, acc) => tco(() => {
    if (n < 2) {
      return acc;
    } else {
      return fact(n-1, n * acc);
    }
  });

  return fact(n, 1).execute();
}

console.log(factorial(2000000)); // Infinity

As you can see, this allows you to write tail recursive functions with only a small difference in syntax, without running into a max call stack error.

tjjfvi
  • 409
  • 5
  • 6
19

In theory yes. As the other answer states.

In practice though, as of July 2017, No. Only Safari supports it.

Javascript ES6 (ES2015) compatability: https://kangax.github.io/compat-table/es6/

AK_
  • 7,981
  • 7
  • 46
  • 78
  • 5
    Correct - and since Chrome is not working on it at all, there appears to be an excellent probability that tail calls will never be usable in production code. https://www.chromestatus.com/feature/5516876633341952 – Freewalker Nov 09 '17 at 16:57
1

Safari is the only browser that supports tail call optimization. ES2015 offers tail call optimization in strict mode

    function factorial(n, returnVal= 1) {
      'use strict';
       if (n <= 1) return returnVal;
        return factorial(n - 1, n * returnVal);
}


factorial(555)

Follow the LINK

subhashis
  • 4,629
  • 8
  • 37
  • 52
0

As of June 2023, there is still no support for TCO in sight in most engines.

Those who would like to implement tail-recursive algorithms and have them optimized, can do that with a utility, such as the one demonstrated by https://stackoverflow.com/a/62376811/7379821

I implemented a similar thing as an npm package:

https://www.npmjs.com/package/@xtao-org/tailrec.js

The nice thing about it is that you can call your tail-recursive function normally -- you just mark the tail calls in the body, like so:

import tailrec from "@xtao-org/tailrec.js"

function factorial(n) {
  const fact = tailrec((n, acc) => {
    if (n < 2) {
      return acc;
    } else {
      return fact.tail(n-1, n * acc);
    }
  })

  return fact(n, 1)
}

I hope it makes life easier for someone. Enjoy!