U Kombinator
Durch Übergeben einer Funktion an sich selbst als Argument kann eine Funktion unter Verwendung ihres Parameters anstelle ihres Namens wiederholt werden! Die gegebene Funktion U
sollte also mindestens einen Parameter haben, der an die Funktion (selbst) gebunden wird.
Im folgenden Beispiel haben wir keine Exit-Bedingung, daher werden wir nur eine unbegrenzte Schleife ausführen, bis ein Stapelüberlauf auftritt
const U = f => f (f) // call function f with itself as an argument
U (f => (console.log ('stack overflow imminent!'), U (f)))
Wir können die unendliche Rekursion mit einer Vielzahl von Techniken stoppen. Hier schreibe ich unsere anonyme Funktion, um eine andere anonyme Funktion zurückzugeben, die auf eine Eingabe wartet. in diesem Fall eine Nummer. Wenn eine Zahl angegeben wird und diese größer als 0 ist, werden wir weiterhin wiederholt, andernfalls wird 0 zurückgegeben.
const log = x => (console.log (x), x)
const U = f => f (f)
// when our function is applied to itself, we get the inner function back
U (f => x => x > 0 ? U (f) (log (x - 1)) : 0)
// returns: (x => x > 0 ? U (f) (log (x - 1)) : 0)
// where f is a reference to our outer function
// watch when we apply an argument to this function, eg 5
U (f => x => x > 0 ? U (f) (log (x - 1)) : 0) (5)
// 4 3 2 1 0
Was hier nicht sofort ersichtlich ist, ist, dass unsere Funktion, wenn sie zum ersten Mal mit dem U
Kombinator auf sich selbst angewendet wird, eine Funktion zurückgibt, die auf die erste Eingabe wartet. Wenn wir dem einen Namen geben, können wir effektiv rekursive Funktionen mit Lambdas (anonyme Funktionen) erstellen.
const log = x => (console.log (x), x)
const U = f => f (f)
const countDown = U (f => x => x > 0 ? U (f) (log (x - 1)) : 0)
countDown (5)
// 4 3 2 1 0
countDown (3)
// 2 1 0
Nur ist dies keine direkte Rekursion - eine Funktion, die sich selbst mit ihrem eigenen Namen aufruft. Unsere Definition von countDown
bezieht sich nicht auf sich selbst in seinem Körper und dennoch ist eine Rekursion möglich
// direct recursion references itself by name
const loop = (params) => {
if (condition)
return someValue
else
// loop references itself to recur...
return loop (adjustedParams)
}
// U combinator does not need a named reference
// no reference to `countDown` inside countDown's definition
const countDown = U (f => x => x > 0 ? U (f) (log (x - 1)) : 0)
Entfernen der Selbstreferenz aus einer vorhandenen Funktion mit dem U-Kombinator
Hier zeige ich Ihnen, wie Sie eine rekursive Funktion, die eine Referenz auf sich selbst verwendet, in eine Funktion ändern, die den U-Kombinator anstelle der Selbstreferenz verwendet
const factorial = x =>
x === 0 ? 1 : x * factorial (x - 1)
console.log (factorial (5)) // 120
Verwenden Sie jetzt den U-Kombinator, um den inneren Verweis auf zu ersetzen factorial
const U = f => f (f)
const factorial = U (f => x =>
x === 0 ? 1 : x * U (f) (x - 1))
console.log (factorial (5)) // 120
Das grundlegende Ersatzmuster ist dieses. Machen Sie sich eine mentale Notiz, wir werden im nächsten Abschnitt eine ähnliche Strategie anwenden
// self reference recursion
const foo = x => ... foo (nextX) ...
// remove self reference with U combinator
const foo = U (f => x => ... U (f) (nextX) ...)
Y Kombinator
verwandt: Die U- und Y-Kombinatoren werden anhand einer Spiegelanalogie erklärt
Im vorherigen Abschnitt haben wir gesehen, wie die Selbstreferenzrekursion mit dem U-Kombinator in eine rekursive Funktion umgewandelt wird, die nicht auf einer benannten Funktion beruht. Es ist ein bisschen ärgerlich, sich daran erinnern zu müssen, die Funktion immer als erstes Argument an sich selbst zu übergeben. Nun, der Y-Kombinator baut auf dem U-Kombinator auf und entfernt dieses mühsame Stück. Dies ist eine gute Sache, da das Entfernen / Reduzieren der Komplexität der Hauptgrund ist, warum wir Funktionen erstellen
Lassen Sie uns zunächst unseren eigenen Y-Kombinator ableiten
// standard definition
const Y = f => f (Y (f))
// prevent immediate infinite recursion in applicative order language (JS)
const Y = f => f (x => Y (f) (x))
// remove reference to self using U combinator
const Y = U (h => f => f (x => U (h) (f) (x)))
Jetzt werden wir sehen, wie die Verwendung im Vergleich zum U-Kombinator ist. Beachten Sie, um zu wiederholen, anstatt U (f)
wir einfach anrufen könnenf ()
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
Y (f => (console.log ('stack overflow imminent!'), f ()))
Jetzt werde ich das countDown
Programm mit demonstrieren Y
- Sie werden sehen, dass die Programme fast identisch sind, aber der Y-Kombinator hält die Dinge ein bisschen sauberer
const log = x => (console.log (x), x)
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const countDown = Y (f => x => x > 0 ? f (log (x - 1)) : 0)
countDown (5)
// 4 3 2 1 0
countDown (3)
// 2 1 0
Und jetzt werden wir sehen , factorial
wie gut
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const factorial = Y (f => x =>
x === 0 ? 1 : x * f (x - 1))
console.log (factorial (5)) // 120
Wie Sie sehen können, f
wird der Mechanismus für die Rekursion selbst. Um es noch einmal zu wiederholen, nennen wir es wie eine gewöhnliche Funktion. Wir können es mehrmals mit verschiedenen Argumenten aufrufen und das Ergebnis wird immer noch korrekt sein. Und da es sich um eine gewöhnliche Funktionsparameter ist, können wir es nennen , was wir wollen, wie recur
unten -
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const fibonacci = Y (recur => n =>
n < 2 ? n : recur (n - 1) + (n - 2))
console.log (fibonacci (10)) // 55
U- und Y-Kombinator mit mehr als 1 Parameter
In den obigen Beispielen haben wir gesehen, wie wir ein Argument schleifen und übergeben können, um den "Zustand" unserer Berechnung zu verfolgen. Aber was ist, wenn wir den zusätzlichen Zustand im Auge behalten müssen?
Wir könnten zusammengesetzte Daten wie ein Array oder so etwas verwenden ...
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const fibonacci = Y (f => ([a, b, x]) =>
x === 0 ? a : f ([b, a + b, x - 1]))
// starting with 0 and 1, generate the 7th number in the sequence
console.log (fibonacci ([0, 1, 7]))
// 0 1 1 2 3 5 8 13
Dies ist jedoch schlecht, da der interne Status (Zähler a
und b
) angezeigt wird . Es wäre schön, wenn wir einfach anrufen könnten fibonacci (7)
, um die gewünschte Antwort zu erhalten.
Mit dem, was wir über Curry-Funktionen wissen (Sequenzen von unären (1-Parameter-) Funktionen), können wir unser Ziel leicht erreichen, ohne unsere Definition von Y
zusammengesetzten Daten oder erweiterten Sprachmerkmalen ändern oder uns darauf verlassen zu müssen.
Schauen Sie sich die Definition von fibonacci
unten an. Wir sind sofort anwenden 0
und 1
die an gebunden sind a
und b
jeweils. Jetzt wartet Fibonacci einfach darauf, dass das letzte Argument geliefert wird, an das gebunden wird x
. Wenn wir wiederkehren, müssen wir f (a) (b) (x)
(nicht f (a,b,x)
) aufrufen, weil unsere Funktion in Curry-Form vorliegt.
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const fibonacci = Y (f => a => b => x =>
x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1)
console.log (fibonacci (7))
// 0 1 1 2 3 5 8 13
Diese Art von Muster kann nützlich sein, um alle Arten von Funktionen zu definieren. Unten sehen wir zwei weitere Funktionen, die mit dem Y
Kombinator ( range
und reduce
) und einer Ableitung von reduce
, map
.
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const range = Y (f => acc => min => max =>
min > max ? acc : f ([...acc, min]) (min + 1) (max)) ([])
const reduce = Y (f => g => y => ([x,...xs]) =>
x === undefined ? y : f (g) (g (y) (x)) (xs))
const map = f =>
reduce (ys => x => [...ys, f (x)]) ([])
const add = x => y => x + y
const sq = x => x * x
console.log (range (-2) (2))
// [ -2, -1, 0, 1, 2 ]
console.log (reduce (add) (0) ([1,2,3,4]))
// 10
console.log (map (sq) ([1,2,3,4]))
// [ 1, 4, 9, 16 ]
ES IST ALLES ANONYM OMG
Da wir hier mit reinen Funktionen arbeiten, können wir die Definition durch eine beliebige benannte Funktion ersetzen. Beobachten Sie, was passiert, wenn wir Fibonacci nehmen und benannte Funktionen durch ihre Ausdrücke ersetzen
/* const U = f => f (f)
*
* const Y = U (h => f => f (x => U (h) (f) (x)))
*
* const fibonacci = Y (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1)
*
*/
/*
* given fibonacci (7)
*
* replace fibonacci with its definition
* Y (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
*
* replace Y with its definition
* U (h => f => f (x => U (h) (f) (x))) (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
//
* replace U with its definition
* (f => f (f)) U (h => f => f (x => U (h) (f) (x))) (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
*/
let result =
(f => f (f)) (h => f => f (x => h (h) (f) (x))) (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
console.log (result) // 13
Und da haben Sie es - fibonacci (7)
rekursiv berechnet mit nichts als anonymen Funktionen