tco

npm i tco

npm i tco

Tail call optimization in Node.

https://github.com/rsp/node-tco (readme)

Note: This module may not be needed soon when the ES6 tail call optimization is supported in Node natively.

This module lets you define deeply recursive functions without using any additional memory for stack frames. The recursion depth can grow infinitely and the memory consumption stays constant.

Every function built with this module can call other functions in the same way as it calls itself (mutual recursion is identical to self recursion), and it doesn't have to know whether those functions are themselves build with this module or not. Also, from other functions it can be called like any other function.

This is how it compares to other similar solutions:

Sometimes recursion can be conceptually preferable to describe certain problems. But to have an arbitrarily deep recursion we have to find a way to implement a recursive function as an iterative process.

Tail call optimization has been known and used for almost 40 years but has been almost completely ignored by the vast majority of programmers.The result is that it is not implemented in most of the programming languages that we use today, including JavaScript.

The problem is that in a language that doesn't optimize tail calls like JavaScript you cannot infinitely call functions inside of functions even in tail positions because every call is another stack frame and you will eventually hit the limit and get an exception:

```
[RangeError: Maximum call stack size exceeded]
```

Even if you go under the limit you are still wasting memory.

This module helps you avoid that problem without the need to rewrite your functions as nested loops.

But because this cannot be done automatically in JavaScript, you will have to wrap your functions with `tco()`

and make a slight change to your return statements:

- change
`return fun(a, b);`

to`return [fun, [a, b]];`

- change
`return val;`

to`return [null, val];`

This is the low level API that is not going to change but there are also some other ways to do the same that may be more convenient in some cases.

This can be greatly simplified using a simple macro:

- change
`return fun(a, b);`

to`ret fun(a, b);`

- change
`return val;`

to`ret val;`

See the Macros section below for details.

Here are some functions for convenience:

`tco.value(val)`

returns`[null, val]`

(more to come)

(Note: See below for a single macro that does the same with a simpler syntax.)

Using those two Sweet.js macros:

```
macro tail {
rule { $f($x:expr (,) ...) } => {
return [$f, [$x (,) ...]]
}
}
macro ret {
rule { $x:expr } => {
return [null, $x]
}
}
```

you can write:

```
tail fun(a, b);
// ...
ret val;
```

instead of:

```
return [fun, [a, b]];
// ...
return [null, val];
```

See: **DEMO**

This may be a much better syntax for the tco module where sweet.js can be used - definitely to be explored in the future.

Another idea for future syntax: instead of those two macros above for tail calls and returning simple values, we could use one macro for both of those cases:

```
macro ret {
rule { $f($x:expr (,) ...) } => {
return [$f, [$x (,) ...]]
}
rule { $x:expr } => {
return [null, $x]
}
}
```

Now the only difference of optimized function body as compared to a normal function would be changing `return`

keywords to `ret`

.

See **DEMO**

Example:

```
var teven = tco(function (n) {
if (n == 0) ret true;
else ret todd(n - 1);
});
var todd = tco(function (n) {
if (n == 0) ret false;
else ret teven(n - 1);
});
```

is converted by the `ret`

macro into:

```
var teven = tco(function (n) {
if (n == 0) return [null, true];
else return [todd, [n - 1]];
});
var todd = tco(function (n) {
if (n == 0) return [null, false];
else return [teven, [n - 1]];
});
```

See: **DEMO**

And it works correctly - see example4.sjs.

This is not perfect but it works and maintains certain important properies described in section Philosophy.

Hopefully one day JavaScript will get real tail call optimization and this will no longer be needed.

A function built with this module has the following properties:

- other functions can call it as a normal function (they don't have to be aware whether it is tco-optimized or not)
- it can call recursively other functions in exactly the same way as it calls itself (no special case for self-recursion)
- it can call other non-optimized functions in the same way as it calls tco-optimized functions (it doesn't have to be aware whether other functions are optimized or not)

The cost of those properties is that the tco-optimized function needs to be aware that it is itself tco-optimized (you need to use special syntax but only for return statements).

Some other solutions take a different approach where the optimized function needs no changes to its code but it needs to be called in a special way by other optimized functions, making mutual recursion much harder.

With this module the optimized function can call other functions in the same way as it calls itself, and it doesn't have to know whether those functions are optimized or not. It means that all needed changes are contained inside the optimized function body and are needed only for its return statements.

```
var tco = require('tco');
// normal recursive function:
var nrec = function (n, max) {
if (n < max)
return nrec(n+1, max);
else
return n;
};
// tco recursive function:
var trec = tco(function (n, max) {
if (n < max)
return [trec, [n+1, max]];
else
return [null, n];
});
// helper function to check for errors:
function run(f, max, t) {
try {
console.log(t+':');
console.log(f(0, max));
} catch(e) {
console.log(e);
}
}
// run both functions:
var max = 1000;
run(nrec, max, 'normal recursion');
run(trec, max, 'tco recursion');
```

For `max = 1000`

this code will print:

```
normal recursion:
1000
tco recursion:
1000
```

For `max = 10000`

it is still fine:

```
normal recursion:
10000
tco recursion:
10000
```

But for `max = 100000`

we get:

```
normal recursion:
[RangeError: Maximum call stack size exceeded]
tco recursion:
100000
```

Even if we set the maximum recursion depth to a billion it will take a lot of time but it will still work and not use more memory than with `max = 5`

or anyhning else.

The mutual recursion works in exactly the same way as simple self-recursion:

```
// normal recursive function:
var neven = function (n) {
if (n == 0) return true;
else return nodd(n - 1);
};
var nodd = function (n) {
if (n == 0) return false;
else return neven(n - 1);
};
// tco recursive function:
var teven = tco(function (n) {
if (n == 0) return [null, true];
else return [todd, [n - 1]];
});
var todd = tco(function (n) {
if (n == 0) return [null, false];
else return [teven, [n - 1]];
});
```

See example2.js for more details.

Install to use in your Node project, updating the dependencies in package.json:

```
npm install tco --save
```

Example with CDN:

```
<script src="https://cdn.rawgit.com/rsp/node-tco/v0.0.12/tco.min.js"></script>
```

This is work in progress - more to come.

For any bug reports or feature requests please post an issue on GitHub.

This is **free software** released under the Expat (MIT) License.

It is a permissive, non-copyleft, GPL-compatible license that complies with:

See LICENSE.md for details.

6yrs ago

1

0

1

0

Version | Tag | Published |
---|---|---|

0.0.15 | latest | 6yrs ago |

No alternatives found

No tutorials found

Add a tutorialMenu