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:
- robi lub nie węzeł.js do TCO?
- Jak to magiczne
yield
działa w Node.js?
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łaniacounter
jest inicjalizowany i natychmiast odzyskujemy iterator; żaden z rzeczywistego kodu wcounter
nie działa (jeszcze). - wywołanie
it.next()
Działa kod wcounter
w górę Przez pierwszeyield
stwierdzenie. W tym momenciecounter
zatrzymuje i zachowuje swój stan wewnętrzny.it.next()
zwraca obiekt state ze znacznikiemdone
ivalue
. Jeśli znacznikiemdone
jestfalse
, tovalue
jest wartością przekazaną przez polecenieyield
. - każde wywołanie do
it.next()
przenosi stan wewnątrzcounter
do następnegoyield
. - gdy wywołanie
it.next()
sprawia, żecounter
kończy się i zwraca, obiekt stanu, który otrzymujemy madone
ustawione natrue
ivalue
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.
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
.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.
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.
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