OK!
Der folgende Code wurde mit ES6-Syntax geschrieben, kann aber genauso gut in ES5 oder noch weniger geschrieben werden. ES6 ist nicht erforderlich, um einen "Mechanismus zum x-maligen Schleifen" zu erstellen.
Wenn Sie den Iterator im Rückruf nicht benötigen , ist dies die einfachste Implementierung
const times = x => f => {
if (x > 0) {
f()
times (x - 1) (f)
}
}
// use it
times (3) (() => console.log('hi'))
// or define intermediate functions for reuse
let twice = times (2)
// twice the power !
twice (() => console.log('double vision'))
Wenn Sie den Iterator benötigen , können Sie eine benannte innere Funktion mit einem Zählerparameter verwenden, um für Sie zu iterieren
const times = n => f => {
let iter = i => {
if (i === n) return
f (i)
iter (i + 1)
}
return iter (0)
}
times (3) (i => console.log(i, 'hi'))
Hör hier auf zu lesen, wenn du nicht gerne mehr lernst ...
Aber etwas sollte sich an diesen ...
- Einzelne Verzweigungsanweisungen
if
sind hässlich - was passiert auf der anderen Verzweigung?
- mehrere Anweisungen / Ausdrücke in den Funktionskörpern - werden Prozedurprobleme gemischt?
- implizit zurückgegeben
undefined
- Hinweis auf eine unreine Nebenwirkung
"Gibt es keinen besseren Weg?"
Es gibt. Lassen Sie uns zunächst unsere erste Implementierung erneut betrachten
// times :: Int -> (void -> void) -> void
const times = x => f => {
if (x > 0) {
f() // has to be side-effecting function
times (x - 1) (f)
}
}
Sicher, es ist einfach, aber beachten Sie, wie wir einfach anrufen f()
und nichts damit anfangen. Dies schränkt die Art der Funktion, die wir mehrmals wiederholen können, wirklich ein. Selbst wenn wir den Iterator zur Verfügung haben, f(i)
ist er nicht viel vielseitiger.
Was ist, wenn wir mit einer besseren Art der Funktionswiederholung beginnen? Vielleicht etwas, das Input und Output besser nutzt.
Generische Funktionswiederholung
// repeat :: forall a. Int -> (a -> a) -> a -> a
const repeat = n => f => x => {
if (n > 0)
return repeat (n - 1) (f) (f (x))
else
return x
}
// power :: Int -> Int -> Int
const power = base => exp => {
// repeat <exp> times, <base> * <x>, starting with 1
return repeat (exp) (x => base * x) (1)
}
console.log(power (2) (8))
// => 256
Oben haben wir eine generische repeat
Funktion definiert, die eine zusätzliche Eingabe benötigt, mit der die wiederholte Anwendung einer einzelnen Funktion gestartet wird.
// repeat 3 times, the function f, starting with x ...
var result = repeat (3) (f) (x)
// is the same as ...
var result = f(f(f(x)))
Implementierung times
mitrepeat
Nun, das ist jetzt einfach. Fast die gesamte Arbeit ist bereits erledigt.
// repeat :: forall a. Int -> (a -> a) -> a -> a
const repeat = n => f => x => {
if (n > 0)
return repeat (n - 1) (f) (f (x))
else
return x
}
// times :: Int -> (Int -> Int) -> Int
const times = n=> f=>
repeat (n) (i => (f(i), i + 1)) (0)
// use it
times (3) (i => console.log(i, 'hi'))
Da unsere Funktion i
als Eingabe verwendet und zurückkehrt i + 1
, funktioniert dies effektiv als unser Iterator, an den wir übergebenf
jedes Mal übergeben.
Wir haben auch unsere Aufzählungsliste mit Problemen behoben
- Kein hässlicher Zweig mehr
if
Aussagen
- Körper mit einfachem Ausdruck weisen auf gut getrennte Bedenken hin
- Nicht mehr nutzlos, implizit zurückgegeben
undefined
JavaScript-Kommaoperator, der
Falls Sie Probleme haben zu sehen, wie das letzte Beispiel funktioniert, hängt es davon ab, ob Sie eine der ältesten Kampfachsen von JavaScript kennen. der Kommaoperator - kurz gesagt, er wertet Ausdrücke von links nach rechts aus und gibt den Wert des zuletzt ausgewerteten Ausdrucks zurück
(expr1 :: a, expr2 :: b, expr3 :: c) :: c
In unserem obigen Beispiel verwende ich
(i => (f(i), i + 1))
Das ist nur eine prägnante Art zu schreiben
(i => { f(i); return i + 1 })
Tail Call-Optimierung
So sexy die rekursiven Implementierungen auch sind, an diesem Punkt wäre es für mich unverantwortlich, sie zu empfehlen, da keine JavaScript-VM, die ich mir vorstellen kann, die ordnungsgemäße Eliminierung von Tail Calls unterstützt - Babel, das zum Transpilieren verwendet wird, aber in "defekt; wird neu implementiert" "Status für weit über ein Jahr.
repeat (1e6) (someFunc) (x)
// => RangeError: Maximum call stack size exceeded
Als solches sollten wir unsere Implementierung von überdenken repeat
, um sie stapelsicher zu machen.
Der folgende Code tut verwenden änderbare Variablen n
und x
aber zur Kenntnis , dass alle Mutationen auf die lokalisierten repeat
Funktion - keine staatlichen Veränderungen (Mutationen) sind sichtbar von außerhalb der Funktion
// repeat :: Int -> (a -> a) -> (a -> a)
const repeat = n => f => x =>
{
let m = 0, acc = x
while (m < n)
(m = m + 1, acc = f (acc))
return acc
}
// inc :: Int -> Int
const inc = x =>
x + 1
console.log (repeat (1e8) (inc) (0))
// 100000000
Viele von Ihnen werden sagen: "Aber das funktioniert nicht!" - Ich weiß, entspann dich einfach. Wir können einen Clojure-Stil loop
/ eine Clojure- recur
Schnittstelle für Konstantraumschleifen unter Verwendung reiner Ausdrücke implementieren . keines davonwhile
.
Hier abstrahieren wir while
mit unserer loop
Funktion - sie sucht nach einem speziellen recur
Typ, um die Schleife am Laufen zu halten. Wenn ein Nicht- recur
Typ angetroffen wird, ist die Schleife beendet und das Ergebnis der Berechnung wird zurückgegeben
const recur = (...args) =>
({ type: recur, args })
const loop = f =>
{
let acc = f ()
while (acc.type === recur)
acc = f (...acc.args)
return acc
}
const repeat = $n => f => x =>
loop ((n = $n, acc = x) =>
n === 0
? acc
: recur (n - 1, f (acc)))
const inc = x =>
x + 1
const fibonacci = $n =>
loop ((n = $n, a = 0, b = 1) =>
n === 0
? a
: recur (n - 1, b, a + b))
console.log (repeat (1e7) (inc) (0)) // 10000000
console.log (fibonacci (100)) // 354224848179262000000