Węzeł.js tail-call optimization: możliwe czy nie?

Jak na razie Lubię JavaScript i zdecydowałem się użyć Node.js jako mój silnik częściowo z powodu tego , który twierdzi, że węzeł.js oferuje TCO. Jednak, gdy próbuję uruchomić ten (oczywiście tail-calling) kod z Node.js, powoduje przepełnienie stosu:

function foo(x) {
    if (x == 1) {
        return 1;
    }
    else {
        return foo(x-1);
    }
}

foo(100000);
Poszperałem trochę i znalazłem to . Tutaj wydaje mi się, że powinienem napisać to tak:
function* foo(x) {
    if (x == 1) {
        return 1;
    }
    else {
        yield foo(x-1);
    }
}

foo(100000);

To jednak powoduje błędy składniowe. Próbowałem różnych permutacji tego, ale we wszystkich sprawy, węzeł.js wydaje się niezadowolony z czegoś .

Zasadniczo, chciałbym wiedzieć, co następuje:

  1. robi lub nie węzeł.js do TCO?
  2. Jak to magiczne yield działa w Node.js?
Author: Community, 2014-04-24

4 answers

Są tu dwa dość odrębne pytania:

  • robi lub nie węzeł.js do TCO?
  • Jak to magiczne plonowanie działa w Node.js?

Robi lub nie węzeł.js do TCO?

TL; DR: już nie, od węzła 8.x . Tak było przez jakiś czas, za jedną lub inną flagą, ale w momencie pisania tego tekstu (listopad 2017) już nie, ponieważ podstawowy silnik JavaScript V8, którego używa, nie obsługuje TCO już nie. Zobacz ta odpowiedź aby dowiedzieć się więcej na ten temat.

Szczegóły:

Optymalizacja połączeń ogonowych (TCO) jest wymaganą częścią specyfikacji ES2015 ("ES6") . Więc wspieranie go nie jest, bezpośrednio, rzeczą NodeJS, to coś, co silnik V8 JavaScript, że NodeJS używa musi obsługiwać.

Od węzła 8.x, V8 nie obsługuje TCO, nawet za flagą. Może to zrobić (ponownie) w pewnym momencie w przyszłości; Zobacz ta odpowiedź więcej na to.

Node 7.10 przynajmniej do 6.5.0 (moje notatki mówią 6.2, ale node.green nie zgadza się) obsługiwał TCO za flagą (--harmony w 6.6.0 i nowszych, --harmony_tailcalls wcześniej) tylko w trybie ścisłym.

Jeśli chcesz sprawdzić swoją instalację, tutaj są testy węzeł.zielony używa (pamiętaj, aby użyć flagi, jeśli używasz odpowiedniej wersji):

function direct() {
    "use strict";
    return (function f(n){
      if (n <= 0) {
        return  "foo";
      }
      return f(n - 1);
    }(1e6)) === "foo";
}

function mutual() {
    "use strict";
    function f(n){
      if (n <= 0) {
        return  "foo";
      }
      return g(n - 1);
    }
    function g(n){
      if (n <= 0) {
        return  "bar";
      }
      return f(n - 1);
    }
    return f(1e6) === "foo" && f(1e6+1) === "bar";
}

console.log(direct());
console.log(mutual());
$ # Only certain versions of Node, notably not 8.x or (currently) 9.x; see above
$ node --harmony tco.js
true
true

Jak działa ta magiczna rzecz yield w Node.js?

To jest inny ES2015 ("funkcje generatora"), więc znowu jest to coś, co V8 musi zaimplementować. Jest całkowicie zaimplementowany w wersji V8 w Node 6.6.0 (i był przez kilka wersji) i nie ma żadnych flag.

Funkcje generatora (pisane za pomocą function* i używając yield) działają poprzez możliwość zatrzymania i zwrócenia iteratora, który przechwytuje ich stan i może być użyty do kontynuowania ich stanu przy następnej okazji. Alex Rauschmeyer ma na ich temat obszerny artykuł tutaj .

Oto przykład użycia iteratora zwracanego przez funkcję generatora jawnie, ale zazwyczaj tego nie zrobisz i za chwilę zobaczymy dlaczego:

"use strict";
function* counter(from, to) {
    let n = from;
    do {
        yield n;
    }
    while (++n < to);
}

let it = counter(0, 5);
for (let state = it.next(); !state.done; state = it.next()) {
    console.log(state.value);
}

Który ma taki wynik:

0
1
2
3
4

Oto jak to działa:

  • kiedy wołamy counter (let it = counter(0, 5);), początkowy wewnętrzny stan wywołania counter jest inicjalizowany i natychmiast odzyskujemy iterator; żaden z rzeczywistego kodu w counter nie działa (jeszcze).
  • wywołanie it.next() Działa kod w counter w górę Przez pierwsze yield stwierdzenie. W tym momencie counter zatrzymuje i zachowuje swój stan wewnętrzny. it.next() zwraca obiekt state ze znacznikiem done i value. Jeśli znacznikiem done jest false, to value jest wartością przekazaną przez polecenie yield.
  • każde wywołanie do it.next() przenosi stan wewnątrz counter do następnego yield.
  • gdy wywołanie it.next() sprawia, że counter kończy się i zwraca, obiekt stanu, który otrzymujemy ma done ustawione na true i value ustawione na return wartość counter.

Posiadanie zmiennych dla iteratora i obiektu state oraz wykonywanie wywołań do it.next() i uzyskiwanie dostępu do właściwości done i value to wszystko boilerplate, które (zazwyczaj) przeszkadza nam w tym, co próbujemy zrobić, więc ES2015 dostarcza nowe for-of oświadczenie, które usuwa wszystko za nas i po prostu daje nam każdą wartość. Oto ten sam kod powyżej napisany for-of:

"use strict";
function* counter(from, to) {
    let n = from;
    do {
        yield n;
    }
    while (++n < to);
}

for (let v of counter(0, 5)) {
    console.log(v);
}

v odpowiada state.value w naszym poprzednim przykładzie, z for-of robiąc wszystkie it.next() dzwoni i done sprawdza dla nas.

 47
Author: T.J. Crowder,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-09 18:02:34

Węzeł.js w końcu obsługuje TCO od 2016.05.17, Wersja 6.2.0.

Aby TCO działało, musi być wykonane z użyciem znaczników --use-strict --harmony-tailcalls.
 4
Author: yairchu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-05-22 12:40:29

6.2.0-with 'use strict' and '--harmony_tailcalls '

Działa tylko z małymi rekurencjami zoptymalizowanymi pod kątem ogona 10000 (jak w pytaniu), ale funkcja nie wywołuje się 99999999999999 razy.

7.2.0 z 'use strict' i '--harmony '

Flaga działa bezproblemowo i szybko nawet przy wywołaniach 999999999999999.

 1
Author: Mihey Mik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-14 21:12:34

Bardziej zwięzła odpowiedź... od daty realizacji, jak wspomniano...

TCO działa. Nie jest kuloodporny, ale jest bardzo przyzwoity. Oto Faktoria (7000000,1).
>node --harmony-tailcalls -e "'use strict';function f(n,t){return n===1?t:f(n-1,n*t);}; console.log(f(7000000,1))"
Infinity

I tu jest bez TCO.

>node -e "'use strict';function f(n,t){return n===1?t:f(n-1,n*t);}; console.log(f(15669,1))"
[eval]:1
function f(n,t){return n===1?t:f(n-1,n*t);}; console.log(f(15669,1))
      ^

RangeError: Maximum call stack size exceeded
at f ([eval]:1:11)
at f ([eval]:1:32)
at f ([eval]:1:32)
at ...
/ Align = "left" / 15668

Jeśli chodzi o yield, zobacz inne odpowiedzi. To powinno być oddzielone pytanie.

 -1
Author: TylerY86,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-26 11:02:44