Jak zastąpić pętle while alternatywą programowania funkcyjnego bez optymalizacji wywołania ogonowego?
Eksperymentuję z bardziej funkcjonalnym stylem w moim JavaScript; dlatego zastąpiłem pętle funkcjami narzędziowymi, takimi jak map i reduce. Jednak nie znalazłem funkcjonalnego zamiennika dla pętli while, ponieważ optymalizacja wywołań ogonowych jest ogólnie niedostępna dla JavaScript. (Z tego co rozumiem ES6 zapobiega przepełnieniu stosu wywołaniami ogonowymi, ale nie optymalizuje ich wydajności.)
Wyjaśniam, co próbowałem poniżej, ale TLDR jest: Jeśli nie mam optymalizacja połączeń ogonowych, jaki jest funkcjonalny sposób implementacji pętli while?
Czego próbowałem:
Tworzenie funkcji użytkowej "while":
function while(func, test, data) {
const newData = func(data);
if(test(newData)) {
return newData;
} else {
return while(func, test, newData);
}
}
Ponieważ optymalizacja wywołania ogonowego nie jest dostępna, mogę przepisać to jako:
function while(func, test, data) {
let newData = *copy the data somehow*
while(test(newData)) {
newData = func(newData);
}
return newData;
}
Jednak w tym momencie wydaje mi się, że mój kod stał się bardziej skomplikowany/mylący dla każdego, kto go używa, ponieważ muszę poruszać się po niestandardowej funkcji użytkowej. Jedyną praktyczną zaletą, jaką widzę, jest to, że zmusza mnie to do pętla czysta; ale wydaje się, że łatwiej byłoby użyć zwykłej pętli while I upewnić się, że wszystko jest czyste.
Próbowałem również znaleźć sposób na stworzenie funkcji generatora, która naśladuje efekty rekurencji/pętli, a następnie iterację nad nią za pomocą funkcji użytkowej, takiej jak find lub reduce. Jednak nie wymyśliłem jeszcze czytelnego sposobu, aby to zrobić.
Wreszcie, zastąpienie pętli funkcjami narzędziowymi sprawia, że bardziej widoczne jest to, czym jestem próba wykonania (np. wykonanie rzeczy z każdym elementem, sprawdzenie czy każdy element przechodzi test, itp.). Wydaje mi się jednak, że pętla while wyraża już to co staram się osiągnąć (np. iteracja aż znajdziemy liczbę pierwszą, iteracja aż odpowiedź będzie wystarczająco zoptymalizowana, itp.).
Więc po tym wszystkim, moje ogólne Pytanie brzmi: jeśli potrzebuję pętli while, programuję w stylu funkcjonalnym i nie mam dostępu do optymalizacji połączeń ogonowych, to co jest najlepsze strategia.
3 answers
Przykład w języku JavaScript
Oto przykład użycia JavaScript. Obecnie większość przeglądarek nie obsługuje optymalizacji wywołań ogonowych, dlatego następujący fragment nie powiedzie się
const repeat = n => f => x =>
n === 0 ? x : repeat (n - 1) (f) (f(x))
console.log(repeat(1e3) (x => x + 1) (0)) // 1000
console.log(repeat(1e5) (x => x + 1) (0)) // Error: Uncaught RangeError: Maximum call stack size exceeded
Trampoliny
Możemy obejść to ograniczenie, zmieniając sposób zapisu powtarzania, ale tylko nieznacznie. Zamiast zwracać wartość bezpośrednio lub natychmiast powtarzające, zwrócimy jeden z naszych dwóch typów trampolina, Bounce
lub Done
. Następnie użyjemy naszej trampoline
funkcji do obsługi pętli.
// trampoline
const Bounce = (f,x) => ({ isBounce: true, f, x })
const Done = x => ({ isBounce: false, x })
const trampoline = ({ isBounce, f, x }) => {
while (isBounce)
({ isBounce, f, x } = f(x))
return x
}
// our revised repeat function, now stack-safe
const repeat = n => f => x =>
n === 0 ? Done(x) : Bounce(repeat (n - 1) (f), f(x))
// apply trampoline to the result of an ordinary call repeat
let result = trampoline(repeat(1e6) (x => x + 1) (0))
// no more stack overflow
console.log(result) // 1000000
Currying spowalnia też trochę sprawy, ale możemy temu zaradzić używając funkcji pomocniczej do rekurencji. Jest to również miłe, ponieważ ukrywa szczegóły implementacji trampoliny i nie oczekuje, że wywołujący odbije wartość zwracaną. To działa około dwa razy szybciej niż powyżej repeat
// aux helper hides trampoline implementation detail
// runs about 2x as fast
const repeat = n => f => x => {
const aux = (n, x) =>
n === 0 ? Done(x) : Bounce(x => aux (n - 1, x), f (x))
return trampoline (aux (n, x))
}
Clojure-styl loop
/recur
Trampoliny są ładne i w ogóle, ale to trochę denerwujące, że trzeba się martwić wywołaniem trampoline
na wartość zwracaną funkcji. Widzieliśmy, że alternatywą było użycie pomocniczego pomocnika, ale to też trochę denerwujące. Jestem pewien, że niektórzy z Was nie przepadają za opakowaniami Bounce
i Done
.
Clojure tworzy wyspecjalizowany interfejs trampoliny, który wykorzystuje parę funkcji, loop
i {[17] – - Ta para tandemów nadaje się do niezwykle eleganckie wyrażenie programu
Oh and it ' s really fast, too
const recur = (...args) =>
({ type: recur, args })
const loop = f =>
{
let acc = f ()
while (acc && acc.type === recur)
acc = f (...acc.args)
return acc
}
const repeat = $n => $f => $x =>
loop ((n = $n, f = $f, x = $x) =>
n === 0
? x
: recur (n - 1, f, f (x)))
console.time ('loop/recur')
console.log (repeat (1e6) (x => x + 1) (0)) // 1000000
console.timeEnd ('loop/recur') // 24 ms
Kontynuacja monady
To jeden z moich ulubionych tematów, więc zobaczymy jak to będzie wyglądało w kontynuacji monady. Pójdziemy również krok dalej i ukryjemy szczegóły implementacji trampoliny wewnątrz naszej funkcji repeat
za pomocą funkcji pomocniczej (aux
), aby rozmówca nie musiał się martwić o odbicie wartość zwracana za każdym razem
// trampoline
const Bounce = (f,x) => ({ isBounce: true, f, x })
const trampoline = t => {
while (t && t.isBounce)
t = t.f(t.x)
return t
}
// Continuation monad; stack-safe implementation
const Cont = f => ({
_runCont: f,
chain: g =>
Cont(k =>
Bounce(f, x =>
Bounce(g(x)._runCont, k)))
})
Cont.of = x => Cont(k => k(x))
const runCont = (m,k) =>
trampoline(Bounce(m._runCont, k))
// repeat now leaks no implementation detail that a trampoline is used
const repeat = n => f => x => {
const aux = (n,x) =>
n === 0 ? Cont.of(x) : Cont.of(f(x)).chain(x => aux(n - 1, x))
return runCont(aux(n,x), x => x)
}
// looks like any other function, still stack-safe
console.log(repeat(1e5) (x => x + 1) (0))
Y
kombinator
Kombinator Y jest moim kombinatorem ducha; ta odpowiedź byłaby niekompletna, nie dając jej miejsca wśród innych technik. Większość implementacji kombinatora Y nie jest jednak bezpieczna dla stosu i przepełni się, jeśli dostarczona przez użytkownika funkcja powtarza się zbyt wiele razy. Ponieważ ta odpowiedź dotyczy zachowania bezpiecznego stosu, oczywiście zaimplementujemy Y
w bezpieczny sposób-ponownie, polegając na naszej zaufanej trampolinie.
Y
demonstruje możliwość rozszerzenia łatwej w użyciu, bezpiecznej dla stosu, synchronicznej nieskończonej rekurencji bez zaśmiecania funkcji.
const bounce = f => (...xs) =>
({ isBounce: true, f, xs })
const trampoline = t => {
while (t && t.isBounce)
t = t.f(...t.xs)
return t
}
// stack-safe Y combinator
const Y = f => {
const safeY = f =>
bounce((...xs) => f (safeY (f), ...xs))
return (...xs) =>
trampoline (safeY (f) (...xs))
}
// recur safely to your heart's content
const repeat = Y ((recur, n, f, x) =>
n === 0
? x
: recur (n - 1, f, f (x)))
console.log(repeat (1e5, x => x + 1, 0)) // 10000
Praktyczność z pętlą while
Ale bądźmy szczerzy: to dużo ceremonii, gdy pomijamy jedno z bardziej oczywistych potencjalnych rozwiązań: użyj pętli for
lub while
, ale ukryj ją za funkcjonalnym interfejs
Dla wszystkich intencji i celów, ta repeat
funkcja działa identycznie do tych podanych powyżej-z tym, że ta jest około jeden lub dwa razy szybsza (z wyjątkiem loop
/recur
rozwiązanie). Heck, to jest prawdopodobnie dużo łatwiejsze do odczytania zbyt.
Oczywiście, funkcja ta jest być może wymyślonym przykładem – nie wszystkie funkcje rekurencyjne można tak łatwo przekształcić w pętlę for
lub while
, ale w takim scenariuszu, w którym jest to możliwe, prawdopodobnie najlepiej jest po prostu zrób to tak. Zapisz trampoliny i kontynuacje do ciężkiego podnoszenia, gdy prosta pętla nie wystarczy.
const repeat = n => f => x => {
let m = n
while (true) {
if (m === 0)
return x
else
(m = m - 1, x = f (x))
}
}
const gadzillionTimes = repeat(1e8)
const add1 = x => x + 1
const result = gadzillionTimes (add1) (0)
console.log(result) // 100000000
setTimeout
nie jest rozwiązaniem problemu przepełnienia stosu
OK, więc to działa , ale tylko paradoksalnie. Jeśli twój zestaw danych jest mały, nie potrzebujesz setTimeout
, ponieważ nie będzie przepełnienia stosu. Jeśli twój zbiór danych jest duży i używasz setTimeout
jako bezpiecznego mechanizmu rekurencji, nie tylko uniemożliwiasz synchronicznie Zwraca wartość z funkcji, będzie ona tak wolna, że nawet nie będziesz chciał jej używać]}
Niektórzy ludzie znaleźli stronę z pytaniami o wywiad, która zachęca do tej strasznej strategii {59]}
Jak wyglądałby nasz repeat
używając setTimeout
- zauważ, że jest on również zdefiniowany w stylu continuation passing-ie, musimy wywołać repeat
z wywołaniem zwrotnym (k
), aby uzyskać ostateczną wartość
// do NOT implement recursion using setTimeout
const repeat = n => f => x => k =>
n === 0
? k (x)
: setTimeout (x => repeat (n - 1) (f) (x) (k), 0, f (x))
// be patient, this one takes about 5 seconds, even for just 1000 recursions
repeat (1e3) (x => x + 1) (0) (console.log)
// comment the next line out for absolute madness
// 10,000 recursions will take ~1 MINUTE to complete
// paradoxically, direct recursion can compute this in a few milliseconds
// setTimeout is NOT a fix for the problem
// -----------------------------------------------------------------------------
// repeat (1e4) (x => x + 1) (0) (console.log)
Nie mogę wystarczająco podkreślić, jak źle to jest. Nawet bieganie trwa tak długo, że przestałem próbować go zmierzyć. Nie uwzględniam tego w poniższych benchmarkach, ponieważ jest to po prostu zbyt powolne, aby nawet uznać je za realne podejście.
Obietnice
Obietnice mają możliwość łączenia obliczeń i są bezpieczne dla stosu. Jednak osiągnięcie bezpiecznego stosu Warto zauważyć w tym rozwiązaniu, szczegóły implementacji obietnicy są całkowicie ukryte przed rozmówcą. Pojedyncza kontynuacja jest dostarczana jako czwarty argument i jest wywoływana, gdy obliczenia są kompletna. Benchmarki Poważnie, Epoka Kamienia Powyższe techniki są demonstrowane przy użyciu nowszych składni ES6, ale można zaimplementować trampolinę w najwcześniejszej możliwej wersji JavaScript – to tylko wymaga Poniżej, używamy JavaScript epoki kamienia, aby wykazać, że nieskończona rekurencja jest możliwa i wykonywana bez koniecznie poświęcając synchroniczną wartość zwracaną– 100,000,000 rekurencje w pod 6 sekund - jest to dramatyczna różnica w porównaniu do Nieblokująca rekurencja nieskończona korzystanie z JavaScript epoki kamienia jeśli , z jakiegoś powodu, chcesz nieblokującą (asynchroniczną) rekurencję nieskończoną, możemy polegać na To pokazuje, że nie ma nic specjalnego w wymogu nieblokowania. A W nowoczesnym programie, dane obietnice, zastępowalibyśmy repeat
przy użyciu obietnic oznacza, że będziemy musieli zrezygnować z synchronicznej wartości zwracanej, tak samo jak przy użyciu setTimeout
. Podaję to jako "rozwiązanie", ponieważ rozwiązuje problem, w przeciwieństwie do setTimeout
, ale w sposób bardzo prosty w porównaniu z trampoliną lub kontynuacją monady. Jak można sobie wyobrazić, wydajność jest nieco zła, ale nigdzie nie jest tak zła, jak w powyższym przykładzie setTimeout
{59]}
const repeat = n => f => x => k =>
n === 0
? Promise.resolve(x).then(k)
: Promise.resolve(f(x)).then(x => repeat (n - 1) (f) (x) (k))
// be patient ...
repeat (1e6) (x => x + 1) (0) (x => console.log('done', x))
while
pętla jest lot szybszy - prawie 100x szybciej (porównując najlepsze do najgorszych – ale nie wliczając asynchronicznych odpowiedzi: setTimeout
i Promise
) // sync
// -----------------------------------------------------------------------------
// repeat implemented with basic trampoline
console.time('A')
console.log(tramprepeat(1e6) (x => x + 1) (0))
console.timeEnd('A')
// 1000000
// A 114 ms
// repeat implemented with basic trampoline and aux helper
console.time('B')
console.log(auxrepeat(1e6) (x => x + 1) (0))
console.timeEnd('B')
// 1000000
// B 64 ms
// repeat implemented with cont monad
console.time('C')
console.log(contrepeat(1e6) (x => x + 1) (0))
console.timeEnd('C')
// 1000000
// C 33 ms
// repeat implemented with Y
console.time('Y')
console.log(yrepeat(1e6) (x => x + 1) (0))
console.timeEnd('Y')
// 1000000
// Y 544 ms
// repeat implemented with while loop
console.time('D')
console.log(whilerepeat(1e6) (x => x + 1) (0))
console.timeEnd('D')
// 1000000
// D 4 ms
// async
// -----------------------------------------------------------------------------
// repeat implemented with Promise
console.time('E')
promiserepeat(1e6) (x => x + 1) (0) (console.log)
console.timeEnd('E')
// 1000000
// E 2224 ms
// repeat implemented with setTimeout; FAILED
console.time('F')
timeoutrepeat(1e6) (x => x + 1) (0) (console.log)
console.timeEnd('F')
// ...
// too slow; didn't finish after 3 minutes
while
i funkcji pierwszej klasy setTimeout
, która może tylko 1,000 rekursje w tym samym czasie.function trampoline (t) {
while (t && t.isBounce)
t = t.f (t.x);
return t.x;
}
function bounce (f, x) {
return { isBounce: true, f: f, x: x };
}
function done (x) {
return { isBounce: false, x: x };
}
function repeat (n, f, x) {
function aux (n, x) {
if (n === 0)
return done (x);
else
return bounce (function (x) { return aux (n - 1, x); }, f (x));
}
return trampoline (aux (n, x));
}
console.time('JS1 100K');
console.log (repeat (1e5, function (x) { return x + 1 }, 0));
console.timeEnd('JS1 100K');
// 100000
// JS1 100K: 15ms
console.time('JS1 100M');
console.log (repeat (1e8, function (x) { return x + 1 }, 0));
console.timeEnd('JS1 100M');
// 100000000
// JS1 100K: 5999ms
setTimeout
, aby odroczyć pojedynczą klatkę na początku obliczeń. Ten program używa również JavaScript epoki kamienia i oblicza 100 000 000 rekursji w czasie poniżej 8 sekund, ale tym razem w sposób nieblokujący.while
pętla i pierwsza klasa funkcje są nadal jedynym podstawowym wymogiem do osiągnięcia rekurencji bezpiecznej dla stosu bez utraty wydajności setTimeout
wezwanie do jednej obietnicy.function donek (k, x) {
return { isBounce: false, k: k, x: x };
}
function bouncek (f, x) {
return { isBounce: true, f: f, x: x };
}
function trampolinek (t) {
// setTimeout is called ONCE at the start of the computation
// NOT once per recursion
return setTimeout(function () {
while (t && t.isBounce) {
t = t.f (t.x);
}
return t.k (t.x);
}, 0);
}
// stack-safe infinite recursion, non-blocking, 100,000,000 recursions in under 8 seconds
// now repeatk expects a 4th-argument callback which is called with the asynchronously computed result
function repeatk (n, f, x, k) {
function aux (n, x) {
if (n === 0)
return donek (k, x);
else
return bouncek (function (x) { return aux (n - 1, x); }, f (x));
}
return trampolinek (aux (n, x));
}
console.log('non-blocking line 1')
console.time('non-blocking JS1')
repeatk (1e8, function (x) { return x + 1; }, 0, function (result) {
console.log('non-blocking line 3', result)
console.timeEnd('non-blocking JS1')
})
console.log('non-blocking line 2')
// non-blocking line 1
// non-blocking line 2
// [ synchronous program stops here ]
// [ below this line, asynchronous program continues ]
// non-blocking line 3 100000000
// non-blocking JS1: 7762ms
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
2018-02-19 18:05:21
Programowanie w sensie paradygmatu funkcjonalnego oznacza, że kierujemy się typami, aby wyrazić nasze algorytmy.
Aby przekształcić funkcję rekurencyjną ogona w wersję bezpieczną dla stosu, musimy wziąć pod uwagę dwa przypadki:
- Base case
- przypadek rekurencyjny
Musimy dokonać wyboru, a to idzie dobrze ze związkami zawodowymi. Jednak Javascript nie ma takiego typu danych, więc albo musimy go utworzyć, albo wrócić do kodowania Object
.
Obiekt Encoded
// simulate a tagged union with two Object types
const Loop = x =>
({value: x, done: false});
const Done = x =>
({value: x, done: true});
// trampoline
const tailRec = f => (...args) => {
let step = Loop(args);
do {
step = f(Loop, Done, step.value);
} while (!step.done);
return step.value;
};
// stack-safe function
const repeat = n => f => x =>
tailRec((Loop, Done, [m, y]) => m === 0
? Done(y)
: Loop([m - 1, f(y)])) (n, x);
// run...
const inc = n =>
n + 1;
console.time();
console.log(repeat(1e6) (inc) (0));
console.timeEnd();
Function Encoded
Alternatywnie, możemy utworzyć rzeczywisty tagowany związek z kodowaniem funkcji. Teraz nasz styl jest znacznie bliższy dojrzałym językom funkcyjnym: {]}
// type/data constructor
const Type = Tcons => (tag, Dcons) => {
const t = new Tcons();
t.run = cases => Dcons(cases);
t.tag = tag;
return t;
};
// tagged union specific for the case
const Step = Type(function Step() {});
const Done = x =>
Step("Done", cases => cases.Done(x));
const Loop = args =>
Step("Loop", cases => cases.Loop(args));
// trampoline
const tailRec = f => (...args) => {
let step = Loop(args);
do {
step = f(step);
} while (step.tag === "Loop");
return step.run({Done: id});
};
// stack-safe function
const repeat = n => f => x =>
tailRec(step => step.run({
Loop: ([m, y]) => m === 0 ? Done(y) : Loop([m - 1, f(y)]),
Done: y => Done(y)
})) (n, x);
// run...
const inc = n => n + 1;
const id = x => x;
console.log(repeat(1e6) (inc) (0));
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
2018-02-28 19:43:20
Zobacz także rozwiń które (z Ramda docs)
Buduje listę z wartości zalążkowej. Przyjmuje funkcję iteratora, która zwraca false, aby zatrzymać iterację lub tablicę o długości 2 zawierający wartość dodaną do listy wynikowej i zalążek, który ma być używany w następnym wywołaniu funkcji iteratora.
var r = n => f => x => x > n ? false : [x, f(x)];
var repeatUntilGreaterThan = n => f => R.unfold(r(n)(f), 1);
console.log(repeatUntilGreaterThan(10)(x => x + 1));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.22.1/ramda.min.js"></script>
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
2018-01-11 11:52:47