Was ist ein Y-Kombinator? [geschlossen]


392

Ein Y-Kombinator ist ein Informatikkonzept von der „funktionalen“ Seite der Dinge. Die meisten Programmierer wissen überhaupt nicht viel über Kombinatoren, wenn sie überhaupt davon gehört haben.

  • Was ist ein Y-Kombinator?
  • Wie funktionieren Kombinatoren?
  • Wofür sind sie gut?
  • Sind sie in prozeduralen Sprachen nützlich?

12
Ein kleiner Tipp: Wenn Sie wie ich etwas über funktionale Sprachen lernen, lassen Sie Kombinatoren besser, bis Sie sich damit
vertraut gemacht haben

3
Ich muss den Gravatar des Herausgebers dieser Frage anlächeln :) In Verbindung stehender Link auf Mads Torgensens Blog
Benjol


1
Ich schrieb einen kurzen Überblick über mein Verständnis des Y-Kombinators: gist.github.com/houtianze/b274e4b975a28fe08aee681699c3f7d0 Ich erklärte (nach meinem Verständnis), wie der "Y-Kombinator eine rekursive Funktion macht"
ibic

1
Wie ist diese Frage "zu weit gefasst"?
Rei Miyasaka

Antworten:


201

Wenn Sie für eine lange Lektüre bereit sind, hat Mike Vanier eine gute Erklärung . Kurz gesagt, es ermöglicht Ihnen, die Rekursion in einer Sprache zu implementieren, die sie nicht unbedingt nativ unterstützt.


14
Es ist jedoch etwas mehr als ein Link; Es ist ein Link mit einer sehr kurzen Zusammenfassung . Eine längere Zusammenfassung wäre willkommen.
Martijn Pieters

2
Es ist nur ein Link, aber es kann nicht besser werden. Diese Antwort verdient (add1 Stimmen) ohne Grundbedingung zum Beenden; aka unendliche Rekursion.
Yavar

7
@Andre MacFie: Ich habe den Aufwand nicht kommentiert, ich habe die Qualität kommentiert. Im Allgemeinen lautet die Richtlinie zum Stapelüberlauf, dass Antworten in sich geschlossen sein sollten und Links zu weiteren Informationen enthalten.
Jørgen Fogh

1
@ Galdre ist richtig. Es ist eine großartige Verbindung, aber es ist nur eine Verbindung. Es wurde auch in 3 anderen Antworten unten erwähnt, aber nur als unterstützendes Dokument, da sie alle gute Erklärungen für sich haben. Diese Antwort versucht auch nicht einmal, die Fragen des OP zu beantworten.
Toraritte

290

Ein Y-Kombinator ist eine "Funktion" (eine Funktion, die andere Funktionen ausführt), die eine Rekursion ermöglicht, wenn Sie nicht von selbst auf die Funktion verweisen können. In der Informatik-Theorie verallgemeinert sie die Rekursion , abstrahiert ihre Implementierung und trennt sie dadurch von der eigentlichen Arbeit der betreffenden Funktion. Der Vorteil, dass für die rekursive Funktion kein Name zur Kompilierungszeit benötigt wird, ist eine Art Bonus. =)

Dies gilt für Sprachen, die Lambda-Funktionen unterstützen . Die ausdrucksbasierte Natur von Lambdas bedeutet normalerweise, dass sie sich nicht namentlich auf sich selbst beziehen können. Und dies zu umgehen, indem man die Variable deklariert, auf sie verweist und ihr dann das Lambda zuweist, um die Selbstreferenzschleife zu vervollständigen, ist spröde. Die Lambda-Variable kann kopiert und die ursprüngliche Variable neu zugewiesen werden, wodurch die Selbstreferenz unterbrochen wird.

Y-Kombinatoren sind in statisch typisierten Sprachen (die häufig prozedurale Sprachen sind) umständlich zu implementieren und häufig zu verwenden, da für Typisierungsbeschränkungen normalerweise die Anzahl der Argumente für die betreffende Funktion zur Kompilierungszeit bekannt sein muss. Dies bedeutet, dass ein y-Kombinator für jede Argumentanzahl geschrieben werden muss, die verwendet werden muss.

Unten finden Sie ein Beispiel für die Verwendung und Funktionsweise eines Y-Kombinators in C #.

Die Verwendung eines Y-Kombinators beinhaltet eine "ungewöhnliche" Art der Konstruktion einer rekursiven Funktion. Zuerst müssen Sie Ihre Funktion als Code schreiben, der eine bereits vorhandene Funktion aufruft und nicht sich selbst:

// Factorial, if func does the same thing as this bit of code...
x == 0 ? 1: x * func(x - 1);

Dann verwandeln Sie das in eine Funktion, die eine Funktion zum Aufrufen benötigt, und geben eine Funktion zurück, die dies tut. Dies wird als Funktion bezeichnet, da es eine Funktion übernimmt und damit eine Operation ausführt, die zu einer anderen Funktion führt.

// A function that creates a factorial, but only if you pass in
// a function that does what the inner function is doing.
Func<Func<Double, Double>, Func<Double, Double>> fact =
  (recurs) =>
    (x) =>
      x == 0 ? 1 : x * recurs(x - 1);

Jetzt haben Sie eine Funktion, die eine Funktion übernimmt und eine andere Funktion zurückgibt, die wie eine Fakultät aussieht, aber anstatt sich selbst aufzurufen, ruft sie das an die äußere Funktion übergebene Argument auf. Wie macht man das zur Fakultät? Übergeben Sie die innere Funktion an sich selbst. Der Y-Kombinator tut dies, indem er eine Funktion mit einem permanenten Namen ist, die die Rekursion einführen kann.

// One-argument Y-Combinator.
public static Func<T, TResult> Y<T, TResult>(Func<Func<T, TResult>, Func<T, TResult>> F)
{
  return
    t =>  // A function that...
      F(  // Calls the factorial creator, passing in...
        Y(F)  // The result of this same Y-combinator function call...
              // (Here is where the recursion is introduced.)
        )
      (t); // And passes the argument into the work function.
}

Anstelle des faktoriellen Aufrufs selbst geschieht, dass der faktorielle Aufruf den faktoriellen Generator aufruft (der durch den rekursiven Aufruf an Y-Combinator zurückgegeben wird). Und abhängig vom aktuellen Wert von t ruft die vom Generator zurückgegebene Funktion den Generator entweder erneut mit t - 1 auf oder gibt nur 1 zurück, wodurch die Rekursion beendet wird.

Es ist kompliziert und kryptisch, aber zur Laufzeit wird alles durcheinander gebracht, und der Schlüssel zu seiner Arbeit ist die "verzögerte Ausführung" und das Aufbrechen der Rekursion, um zwei Funktionen zu umfassen. Das innere F wird als Argument übergeben , das in der nächsten Iteration nur bei Bedarf aufgerufen wird .


5
Warum, warum mussten Sie es "Y" und den Parameter "F" nennen? Sie gehen nur in den Typargumenten verloren!
Brian Henk

3
In Haskell können Sie die Rekursion mit: abstrahieren fix :: (a -> a) -> a, und die aDose kann wiederum von so vielen Argumenten abhängen, wie Sie möchten. Dies bedeutet, dass statisches Tippen dies nicht wirklich umständlich macht.
Peaker

12
Nach der Beschreibung von Mike Vanier ist Ihre Definition für Y eigentlich kein Kombinator, weil sie rekursiv ist. Unter "Eliminieren (der meisten) expliziten Rekursion (Lazy-Version)" hat er das Lazy-Schema-Äquivalent Ihres C # -Codes, erklärt jedoch in Punkt 2: "Es ist kein Kombinator, da das Y im Hauptteil der Definition eine freie Variable ist, die ist erst gebunden, wenn die Definition vollständig ist ... "Ich denke, das Coole an Y-Kombinatoren ist, dass sie durch Auswertung des Fixpunkts einer Funktion eine Rekursion erzeugen. Auf diese Weise benötigen sie keine explizite Rekursion.
GrantJ

@GrantJ Du machst einen guten Punkt. Es ist ein paar Jahre her, seit ich diese Antwort gepostet habe. Wenn ich jetzt Vaniers Beitrag überfliege, sehe ich, dass ich Y geschrieben habe, aber keinen Y-Kombinator. Ich werde seinen Beitrag bald wieder lesen und sehen, ob ich eine Korrektur veröffentlichen kann. Mein Bauch warnt mich, dass die strikte statische Eingabe von C # dies am Ende verhindern könnte, aber ich werde sehen, was ich tun kann.
Chris Ammerman

1
@ WayneBurkett Es ist eine ziemlich übliche Praxis in der Mathematik.
YoTengoUnLCD

102

Ich habe dies von http://www.mail-archive.com/boston-pm@mail.pm.org/msg02716.html aufgehoben. Dies ist eine Erklärung, die ich vor einigen Jahren geschrieben habe.

In diesem Beispiel werde ich JavaScript verwenden, aber viele andere Sprachen funktionieren auch.

Unser Ziel ist es, eine rekursive Funktion von 1 Variablen zu schreiben, indem wir nur Funktionen von 1 Variablen und keine Zuweisungen verwenden, Dinge nach Namen definieren usw. (Warum dies unser Ziel ist, ist eine andere Frage, nehmen wir dies einfach als die Herausforderung, die wir stellen werden gegeben.) Scheint unmöglich, nicht wahr? Lassen Sie uns als Beispiel Fakultät implementieren.

Nun, Schritt 1 ist zu sagen, dass wir dies leicht tun könnten, wenn wir ein wenig schummeln würden. Mit Funktionen von 2 Variablen und Zuweisung können wir zumindest vermeiden, dass die Zuordnung zum Einrichten der Rekursion verwendet werden muss.

// Here's the function that we want to recurse.
X = function (recurse, n) {
  if (0 == n)
    return 1;
  else
    return n * recurse(recurse, n - 1);
};

// This will get X to recurse.
Y = function (builder, n) {
  return builder(builder, n);
};

// Here it is in action.
Y(
  X,
  5
);

Nun wollen wir sehen, ob wir weniger schummeln können. Zunächst verwenden wir die Zuweisung, müssen dies aber nicht. Wir können einfach X und Y inline schreiben.

// No assignment this time.
function (builder, n) {
  return builder(builder, n);
}(
  function (recurse, n) {
    if (0 == n)
      return 1;
    else
      return n * recurse(recurse, n - 1);
  },
  5
);

Wir verwenden jedoch Funktionen von 2 Variablen, um eine Funktion von 1 Variablen zu erhalten. Können wir das beheben? Nun, ein kluger Kerl namens Haskell Curry hat einen ordentlichen Trick. Wenn Sie gute Funktionen höherer Ordnung haben, brauchen Sie nur Funktionen von 1 Variablen. Der Beweis ist, dass Sie mit einer rein mechanischen Texttransformation wie dieser von Funktionen von 2 (oder im allgemeinen Fall mehr) Variablen zu 1 Variablen gelangen können:

// Original
F = function (i, j) {
  ...
};
F(i,j);

// Transformed
F = function (i) { return function (j) {
  ...
}};
F(i)(j);

wo ... bleibt genau das gleiche. (Dieser Trick wird nach seinem Erfinder "Currying" genannt. Die Sprache Haskell wird auch nach Haskell Curry benannt. Datei unter nutzlosen Trivia.) Wenden Sie diese Transformation jetzt einfach überall an und wir erhalten unsere endgültige Version.

// The dreaded Y-combinator in action!
function (builder) { return function (n) {
  return builder(builder)(n);
}}(
  function (recurse) { return function (n) {
    if (0 == n)
      return 1;
    else
      return n * recurse(recurse)(n - 1);
  }})(
  5
);

Fühlen Sie sich frei, es zu versuchen. alert (), die zurückkehren, binden Sie es an einen Knopf, was auch immer. Dieser Code berechnet Fakultäten rekursiv ohne Verwendung von Zuweisungen, Deklarationen oder Funktionen von 2 Variablen. (Wenn Sie jedoch nachverfolgen, wie es funktioniert, dreht sich wahrscheinlich der Kopf. Wenn Sie es ohne Ableitung nur leicht neu formatieren, wird der Code zu einem Code, der Sie verwirren und verwirren wird.)

Sie können die 4 Zeilen, die die Fakultät rekursiv definieren, durch jede andere gewünschte rekursive Funktion ersetzen.


Schöne Erklärung. Warum hast du function (n) { return builder(builder)(n);}statt geschrieben builder(builder)?
v7d8dpo4

@ v7d8dpo4 Weil ich eine Funktion von 2 Variablen mithilfe von Currying in eine Funktion höherer Ordnung einer Variablen umgewandelt habe.
Btilly

Ist das der Grund, warum wir Schließungen brauchen?
TheChetan

1
Mit @TheChetan Closures können wir ein benutzerdefiniertes Verhalten hinter einem Aufruf mit einer anonymen Funktion verknüpfen. Es ist nur eine andere Abstraktionstechnik.
Btilly

85

Ich frage mich, ob es sinnvoll ist, dies von Grund auf aufzubauen. Wir werden sehen. Hier ist eine grundlegende, rekursive Fakultätsfunktion:

function factorial(n) {
    return n == 0 ? 1 : n * factorial(n - 1);
}

Lassen Sie uns eine neue Funktion namens umgestalten und erstellen fact, die eine anonyme Fakultätsberechnungsfunktion zurückgibt, anstatt die Berechnung selbst durchzuführen:

function fact() {
    return function(n) {
        return n == 0 ? 1 : n * fact()(n - 1);
    };
}

var factorial = fact();

Das ist ein bisschen komisch, aber daran ist nichts auszusetzen. Wir generieren nur bei jedem Schritt eine neue Fakultätsfunktion.

Die Rekursion in dieser Phase ist noch ziemlich explizit. Die factFunktion muss ihren eigenen Namen kennen. Lassen Sie uns den rekursiven Aufruf parametrisieren:

function fact(recurse) {
    return function(n) {
        return n == 0 ? 1 : n * recurse(n - 1);
    };
}

function recurser(x) {
    return fact(recurser)(x);
}

var factorial = fact(recurser);

Das ist toll, muss aber recursernoch seinen eigenen Namen kennen. Lassen Sie uns auch das parametrisieren:

function recurser(f) {
    return fact(function(x) {
        return f(f)(x);
    });
}

var factorial = recurser(recurser);

Anstatt recurser(recurser)direkt aufzurufen , erstellen wir jetzt eine Wrapper-Funktion, die das Ergebnis zurückgibt:

function Y() {
    return (function(f) {
        return f(f);
    })(recurser);
}

var factorial = Y();

Wir können jetzt die loswerden recurser Namen insgesamt ; Es ist nur ein Argument für Ys innere Funktion, die durch die Funktion selbst ersetzt werden kann:

function Y() {
    return (function(f) {
        return f(f);
    })(function(f) {
        return fact(function(x) {
            return f(f)(x);
        });
    });
}

var factorial = Y();

Der einzige externe Name, auf den noch verwiesen wird fact, ist , aber es sollte inzwischen klar sein, dass auch dieser leicht zu parametrisieren ist, um die vollständige, generische Lösung zu erstellen:

function Y(le) {
    return (function(f) {
        return f(f);
    })(function(f) {
        return le(function(x) {
            return f(f)(x);
        });
    });
}

var factorial = Y(function(recurse) {
    return function(n) {
        return n == 0 ? 1 : n * recurse(n - 1);
    };
});

Eine ähnliche Erklärung in JavaScript: igstan.ro/posts/…
Pops

1
Sie haben mich verloren, als Sie die Funktion eingeführt haben recurser. Nicht die geringste Ahnung, was es tut oder warum.
Mörre

2
Wir versuchen, eine generische rekursive Lösung für Funktionen aufzubauen, die nicht explizit rekursiv sind. Die recurserFunktion ist der erste Schritt in Richtung dieses Ziels, da sie uns eine rekursive Version gibt, factdie sich niemals namentlich referenziert.
Wayne

@ WayneBurkett, kann ich den Y-Kombinator so umschreiben : function Y(recurse) { return recurse(recurse); } let factorial = Y(creator => value => { return value == 0 ? 1 : value * creator(creator)(value - 1); });. Und so verdaue ich es (nicht sicher, ob es korrekt ist): Indem wir die Funktion nicht explizit referenzieren (als Kombinator nicht zulässig ), können wir zwei teilweise angewendete / Curry- Funktionen (eine Erstellerfunktion und die Berechnungsfunktion) mit verwenden Mit welchen Lambda / anonymen Funktionen können wir rekursiv arbeiten, ohne dass ein Name für die Berechnungsfunktion erforderlich ist?
Neevek

50

Die meisten Antworten oben beschreiben , was der Y-Combinator ist aber nicht das, was es ist für .

Festpunkt combinators werden verwendet um zu zeigen , dass Lambda - Kalkül ist Turing vollständig . Dies ist ein sehr wichtiges Ergebnis in der Berechnungstheorie und bietet eine theoretische Grundlage für die funktionale Programmierung .

Das Studium von Festkomma-Kombinatoren hat mir auch geholfen, die funktionale Programmierung wirklich zu verstehen. Ich habe jedoch nie eine Verwendung für sie in der tatsächlichen Programmierung gefunden.


24

y-Kombinator in JavaScript :

var Y = function(f) {
  return (function(g) {
    return g(g);
  })(function(h) {
    return function() {
      return f(h(h)).apply(null, arguments);
    };
  });
};

var factorial = Y(function(recurse) {
  return function(x) {
    return x == 0 ? 1 : x * recurse(x-1);
  };
});

factorial(5)  // -> 120

Bearbeiten : Ich lerne viel aus dem Betrachten von Code, aber dieser ist ohne Hintergrund etwas schwer zu schlucken - tut mir leid. Mit einigen allgemeinen Kenntnissen, die durch andere Antworten vermittelt werden, können Sie beginnen, herauszufinden, was gerade passiert.

Die Y-Funktion ist der "y-Kombinator". Schauen Sie sich nun die var factorialZeile an, in der Y verwendet wird. Beachten Sie, dass Sie eine Funktion übergeben, die einen Parameter (in diesem Beispiel recurse) enthält, der später auch in der inneren Funktion verwendet wird. Der Parametername wird im Grunde genommen zum Namen der inneren Funktion, die es ihr ermöglicht, einen rekursiven Aufruf auszuführen (da er recurse()in seiner Definition verwendet wird). Der y-Kombinator führt die Magie aus, die ansonsten anonyme innere Funktion mit dem Parameternamen der übergebenen Funktion zu verknüpfen Y. Y.

Die vollständige Erklärung, wie Y die Magie ausübt , finden Sie in dem verlinkten Artikel (übrigens nicht von mir).


6
Javascript benötigt keinen Y-Kombinator, um eine anonyme Rekursion
durchzuführen,

6
arguments.calleeist im strengen Modus nicht erlaubt: developer.mozilla.org/en/JavaScript/…
dave1010

2
Sie können jeder Funktion weiterhin einen Namen geben. Wenn es sich um einen Funktionsausdruck handelt, ist dieser Name nur innerhalb der Funktion selbst bekannt. (function fact(n){ return n <= 1? 1 : n * fact(n-1); })(5)
Esailija


18

Für Programmierer, die noch nicht in der Tiefe auf funktionale Programmierung gestoßen sind und jetzt nicht anfangen möchten, aber leicht neugierig sind:

Der Y-Kombinator ist eine Formel, mit der Sie die Rekursion in einer Situation implementieren können, in der Funktionen keine Namen haben können, sondern als Argumente weitergegeben, als Rückgabewerte verwendet und in anderen Funktionen definiert werden können.

Es funktioniert, indem die Funktion als Argument an sich selbst übergeben wird, damit sie sich selbst aufrufen kann.

Es ist Teil des Lambda-Kalküls, das eigentlich Mathematik ist, aber effektiv eine Programmiersprache ist und für die Informatik und insbesondere für die funktionale Programmierung ziemlich grundlegend ist.

Der tägliche praktische Wert des Y-Kombinators ist begrenzt, da Programmiersprachen dazu neigen, Funktionen zu benennen.

Falls Sie es in einer Polizeiaufstellung identifizieren müssen, sieht es folgendermaßen aus:

Y = λf (λx.f (xx)) (λx.f (xx))

Sie können es normalerweise wegen der wiederholten erkennen (λx.f (x x)) .

Die λSymbole sind der griechische Buchstabe Lambda, der dem Lambda-Kalkül seinen Namen gibt, und es gibt viele (λx.t)Stilbegriffe, weil der Lambda-Kalkül so aussieht.


Dies sollte die akzeptierte Antwort sein. BTW, mit U x = x x, Y = U . (. U)(missbrauchen Haskell-artige Notation). IOW, mit geeigneten Kombinatoren , Y = BU(CBU). Also , Yf = U (f . U) = (f . U) (f . U) = f (U (f . U)) = f ((f . U) (f . U)).
Will Ness

13

Anonyme Rekursion

Ein Festkommakombinator ist eine Funktion höherer Ordnung fix, die per Definition die Äquivalenz erfüllt

forall f.  fix f  =  f (fix f)

fix fstellt eine Lösung xfür die Festpunktgleichung dar

               x  =  f x

Die Fakultät einer natürlichen Zahl kann durch bewiesen werden

fact 0 = 1
fact n = n * fact (n - 1)

Unter Verwendung fixbeliebiger konstruktiver Beweise über allgemeine / μ-rekursive Funktionen können ohne nonymöse Selbstreferenzialität abgeleitet werden.

fact n = (fix fact') n

wo

fact' rec n = if n == 0
                then 1
                else n * rec (n - 1)

so dass

   fact 3
=  (fix fact') 3
=  fact' (fix fact') 3
=  if 3 == 0 then 1 else 3 * (fix fact') (3 - 1)
=  3 * (fix fact') 2
=  3 * fact' (fix fact') 2
=  3 * if 2 == 0 then 1 else 2 * (fix fact') (2 - 1)
=  3 * 2 * (fix fact') 1
=  3 * 2 * fact' (fix fact') 1
=  3 * 2 * if 1 == 0 then 1 else 1 * (fix fact') (1 - 1)
=  3 * 2 * 1 * (fix fact') 0
=  3 * 2 * 1 * fact' (fix fact') 0
=  3 * 2 * 1 * if 0 == 0 then 1 else 0 * (fix fact') (0 - 1)
=  3 * 2 * 1 * 1
=  6

Dieser formale Beweis dafür

fact 3  =  6

Verwendet methodisch die Äquivalenz des Festkomma-Kombinators für das Umschreiben

fix fact'  ->  fact' (fix fact')

Lambda-Kalkül

Der untypisierte Lambda-Kalkül- Formalismus besteht aus einer kontextfreien Grammatik

E ::= v        Variable
   |  λ v. E   Abstraction
   |  E E      Application

wo verstreckt sich über Variablen zusammen mit den Beta und eta Reduktion Regeln

(λ x. B) E  ->  B[x := E]                                 Beta
  λ x. E x  ->  E          if x doesn’t occur free in E   Eta

Die Beta-Reduktion ersetzt alle freien Vorkommen der Variablen xim Abstraktionskörper ("Funktion") Bdurch den Ausdruck ("Argument") E. Eta-Reduktion eliminiert redundante Abstraktion. Es wird manchmal aus dem Formalismus weggelassen. Ein irreduzibler Ausdruck, für den keine Reduktionsregel gilt, liegt in normaler oder kanonischer Form vor .

λ x y. E

ist eine Abkürzung für

λ x. λ y. E

(Abstraktionsmultiarität),

E F G

ist eine Abkürzung für

(E F) G

(Anwendung Linksassoziativität),

λ x. x

und

λ y. y

sind Alpha-Äquivalent .

Abstraktion und Anwendung sind die beiden einzigen „Sprachprimitive“ des Lambda-Kalküls, ermöglichen jedoch die Codierung beliebig komplexer Daten und Operationen.

Die Kirchenzahlen sind eine Kodierung der natürlichen Zahlen, die den peanoaxiomatischen Naturzahlen ähnlich sind.

   0  =  λ f x. x                 No application
   1  =  λ f x. f x               One application
   2  =  λ f x. f (f x)           Twofold
   3  =  λ f x. f (f (f x))       Threefold
    . . .

SUCC  =  λ n f x. f (n f x)       Successor
 ADD  =  λ n m f x. n f (m f x)   Addition
MULT  =  λ n m f x. n (m f) x     Multiplication
    . . .

Ein formeller Beweis dafür

1 + 2  =  3

Verwenden der Umschreiberegel der Beta-Reduzierung:

   ADD                      1            2
=  (λ n m f x. n f (m f x)) (λ g y. g y) (λ h z. h (h z))
=  (λ m f x. (λ g y. g y) f (m f x)) (λ h z. h (h z))
=  (λ m f x. (λ y. f y) (m f x)) (λ h z. h (h z))
=  (λ m f x. f (m f x)) (λ h z. h (h z))
=  λ f x. f ((λ h z. h (h z)) f x)
=  λ f x. f ((λ z. f (f z)) x)
=  λ f x. f (f (f x))                                       Normal form
=  3

Kombinatoren

In der Lambda-Rechnung sind Kombinatoren Abstraktionen, die keine freien Variablen enthalten. Am einfachsten: Ider Identitätskombinator

λ x. x

isomorph zur Identitätsfunktion

id x = x

Solche Kombinatoren sind die primitiven Operatoren von Kombinatorkalkülen wie dem SKI-System.

S  =  λ x y z. x z (y z)
K  =  λ x y. x
I  =  λ x. x

Die Beta-Reduktion normalisiert sich nicht stark . Nicht alle reduzierbaren Ausdrücke, „Redexes“, konvergieren unter Beta-Reduktion zur normalen Form. Ein einfaches Beispiel ist die unterschiedliche Anwendung des Omega- ωKombinators

λ x. x x

zu sich selbst:

   (λ x. x x) (λ y. y y)
=  (λ y. y y) (λ y. y y)
. . .
=  _|_                     Bottom

Die Reduzierung der am weitesten links stehenden Unterausdrücke („Köpfe“) wird priorisiert. Die anwendbare Reihenfolge normalisiert die Argumente vor der Substitution, die normale Reihenfolge nicht. Die beiden Strategien sind analog zu eifriger Bewertung, z. B. C, und fauler Bewertung, z. B. Haskell.

   K          (I a)        (ω ω)
=  (λ k l. k) ((λ i. i) a) ((λ x. x x) (λ y. y y))

divergiert unter eifriger Beta-Reduktion in der Anwendungsreihenfolge

=  (λ k l. k) a ((λ x. x x) (λ y. y y))
=  (λ l. a) ((λ x. x x) (λ y. y y))
=  (λ l. a) ((λ y. y y) (λ y. y y))
. . .
=  _|_

da in strengen Semantik

forall f.  f _|_  =  _|_

konvergiert aber unter fauler Beta-Reduktion normaler Ordnung

=  (λ l. ((λ i. i) a)) ((λ x. x x) (λ y. y y))
=  (λ l. a) ((λ x. x x) (λ y. y y))
=  a

Wenn ein Ausdruck eine normale Form hat, wird er durch eine Beta-Reduktion normaler Ordnung gefunden.

Y.

Die wesentliche Eigenschaft des Y Festkommakombinators

λ f. (λ x. f (x x)) (λ x. f (x x))

ist gegeben durch

   Y g
=  (λ f. (λ x. f (x x)) (λ x. f (x x))) g
=  (λ x. g (x x)) (λ x. g (x x))           =  Y g
=  g ((λ x. g (x x)) (λ x. g (x x)))       =  g (Y g)
=  g (g ((λ x. g (x x)) (λ x. g (x x))))   =  g (g (Y g))
. . .                                      . . .

Die Äquivalenz

Y g  =  g (Y g)

ist isomorph zu

fix f  =  f (fix f)

Der untypisierte Lambda-Kalkül kann beliebige konstruktive Beweise über allgemeine / μ-rekursive Funktionen codieren.

 FACT  =  λ n. Y FACT' n
FACT'  =  λ rec n. if n == 0 then 1 else n * rec (n - 1)

   FACT 3
=  (λ n. Y FACT' n) 3
=  Y FACT' 3
=  FACT' (Y FACT') 3
=  if 3 == 0 then 1 else 3 * (Y FACT') (3 - 1)
=  3 * (Y FACT') (3 - 1)
=  3 * FACT' (Y FACT') 2
=  3 * if 2 == 0 then 1 else 2 * (Y FACT') (2 - 1)
=  3 * 2 * (Y FACT') 1
=  3 * 2 * FACT' (Y FACT') 1
=  3 * 2 * if 1 == 0 then 1 else 1 * (Y FACT') (1 - 1)
=  3 * 2 * 1 * (Y FACT') 0
=  3 * 2 * 1 * FACT' (Y FACT') 0
=  3 * 2 * 1 * if 0 == 0 then 1 else 0 * (Y FACT') (0 - 1)
=  3 * 2 * 1 * 1
=  6

(Multiplikation verzögert, Zusammenfluss)

Für den kirchlichen untypisierten Lambda-Kalkül wurde gezeigt, dass es außerdem eine rekursiv aufzählbare Unendlichkeit von Festpunktkombinatoren gibt Y.

 X  =  λ f. (λ x. x x) (λ x. f (x x))
Y'  =  (λ x y. x y x) (λ y x. y (x y x))
 Z  =  λ f. (λ x. f (λ v. x x v)) (λ x. f (λ v. x x v))
 Θ  =  (λ x y. y (x x y)) (λ x y. y (x x y))
  . . .

Die Beta-Reduktion normaler Ordnung macht den nicht erweiterten untypisierten Lambda-Kalkül zu einem Turing-vollständigen Umschreibungssystem.

In Haskell kann der Festkomma-Kombinator elegant implementiert werden

fix :: forall t. (t -> t) -> t
fix f = f (fix f)

Haskells Faulheit normalisiert sich auf eine Endlichkeit, bevor alle Unterausdrücke ausgewertet wurden.

primes :: Integral t => [t]
primes = sieve [2 ..]
   where
      sieve = fix (\ rec (p : ns) ->
                     p : rec [n | n <- ns
                                , n `rem` p /= 0])


4
Obwohl ich die Gründlichkeit der Antwort schätze, ist sie für einen Programmierer mit wenig formalem mathematischen Hintergrund nach dem ersten Zeilenumbruch in keiner Weise zugänglich.
Jared Smith

4
@ jared-smith Die Antwort soll eine ergänzende Wonkaian-Geschichte über die CS / Mathe-Begriffe hinter dem Y-Kombinator erzählen. Ich denke, dass wahrscheinlich die besten Analogien zu bekannten Konzepten bereits von anderen Antwortenden gezogen wurden. Persönlich war ich immer gern mit dem wahren Ursprung, der radikalen Neuheit einer Idee, über eine schöne Analogie konfrontiert . Ich finde die meisten Analogien unangemessen und verwirrend.

1
Hallo Identitätskombinator λ x . x, wie geht es dir heute?
MaiaVictor

Diese Antwort gefällt mir am besten . Es hat gerade alle meine Fragen geklärt!
Student

11

Andere Antworten geben eine ziemlich präzise Antwort darauf, ohne eine wichtige Tatsache: Sie müssen den Festkomma-Kombinator auf diese verschlungene Weise in keiner praktischen Sprache implementieren und dies dient keinem praktischen Zweck (außer "Schau, ich weiß, welcher Y-Kombinator" ist "). Es ist ein wichtiges theoretisches Konzept, aber von geringem praktischem Wert.


6

Hier ist eine JavaScript-Implementierung des Y-Kombinators und der Factorial-Funktion (aus Douglas Crockfords Artikel, verfügbar unter: http://javascript.crockford.com/little.html ).

function Y(le) {
    return (function (f) {
        return f(f);
    }(function (f) {
        return le(function (x) {
            return f(f)(x);
        });
    }));
}

var factorial = Y(function (fac) {
    return function (n) {
        return n <= 2 ? n : n * fac(n - 1);
    };
});

var number120 = factorial(5);

6

Ein Y-Kombinator ist ein anderer Name für einen Flusskondensator.


4
sehr lustig. :) Junge (er) erkennen die Referenz möglicherweise nicht.
Will Ness

2
Haha! Ja, der Junge (ich) kann immer noch verstehen ...


Ich denke, diese Antwort könnte für nicht englischsprachige Personen besonders verwirrend sein. Man könnte einige Zeit darauf verwenden, diese Behauptung zu verstehen, bevor man (oder nie) merkt, dass es sich um eine humorvolle Referenz der Populärkultur handelt. (Ich mag es, ich würde mich nur schlecht fühlen, wenn ich darauf geantwortet hätte und festgestellt hätte, dass jemand, der etwas lernt, davon entmutigt wird)
Mike

5

Ich habe sowohl in Clojure als auch in Scheme eine Art "Idioten-Leitfaden" für den Y-Kombinator geschrieben, um mich damit auseinanderzusetzen. Sie sind beeinflusst von Material in "The Little Schemer"

Im Schema: https://gist.github.com/z5h/238891

oder Clojure: https://gist.github.com/z5h/5102747

Beide Tutorials sind Code mit Kommentaren durchsetzt und sollten ausgeschnitten und in Ihren Lieblingseditor eingefügt werden können.


5

Als Neuling in Kombinatoren fand ich Mike Vaniers Artikel (danke Nicholas Mancuso) sehr hilfreich. Ich möchte neben der Dokumentation meines Verständnisses eine Zusammenfassung schreiben, wenn es einigen anderen helfen könnte, wäre ich sehr froh.

Von beschissen zu weniger beschissen

Am Beispiel der Fakultät verwenden wir die folgende almost-factorialFunktion, um die Fakultät der Zahl zu berechnen x:

def almost-factorial f x = if iszero x
                           then 1
                           else * x (f (- x 1))

Nimmt almost-factorialim obigen Pseudocode Funktion fund Zahl auf x( almost-factorialwird als Curry verwendet, so dass es als Aufnahme von Funktion fund Rückgabe einer 1-Aritätsfunktion angesehen werden kann).

Wenn almost-factorialberechnet für factorial xes die Berechnung der faktoriellen Delegierten x - 1zu Funktion fund reichert sich dieses Ergebnis mitx (in diesem Fall multipliziert es das Ergebnis von (x - 1) mit x).

Es kann so gesehen werden, dass es almost-factorialeine beschissene Version der Fakultätsfunktion (die nur die Kassenzahl berechnen kann x - 1) aufnimmt und eine weniger beschissene Version der Fakultät zurückgibt (die die Kassenzahl berechnet x). Wie in dieser Form:

almost-factorial crappy-f = less-crappy-f

Wenn wir die weniger beschissene Version von Fakultät wiederholt an übergeben almost-factorial, erhalten wir schließlich unsere gewünschte Fakultätsfunktion f. Wo es betrachtet werden kann als:

almost-factorial f = f

Fixpunkt

Die Tatsache, die almost-factorial f = fbedeutet, fist der Fixpunkt der Funktionalmost-factorial .

Dies war eine wirklich interessante Art, die Beziehungen der oben genannten Funktionen zu sehen, und es war ein Aha-Moment für mich. (Bitte lesen Sie Mikes Beitrag zum Fixpunkt, wenn Sie dies nicht getan haben.)

Drei Funktionen

Zu verallgemeinern, wir haben nicht-rekursive Funktion fn(wie unsere fast-Fakultät), haben wir seine Fix-Punkt - Funktion fr(wie unser f), was dann Ytut , ist , wenn Sie geben Y fn, Ygibt die Fix-Punkt - Funktion fn.

Zusammenfassend (vereinfacht durch die Annahme, dass frnur ein Parameter benötigt wird; xdegeneriert zu x - 1, x - 2... in Rekursion):

  • Wir definieren die Kernberechnungen als fn: def fn fr x = ...accumulate x with result from (fr (- x 1)), Dies ist die fast nützliche Funktion - obwohl wir sie nicht fndirekt verwenden können x, wird sie sehr bald nützlich sein. Dies ist nicht rekursivfn Funktion verwendet eine Funktion fr, um das Ergebnis zu berechnen
  • fn fr = fr, frIst der Fix-Punkt fn, frist die nützliche funciton, können wir verwenden fraufx unser Ergebnis zu erhalten
  • Y fn = fr, Ygibt den Fixpunkt einer Funktion zurück und Y verwandelt unsere fast nützliche Funktion fnin nützlich fr

Ableiten Y (nicht enthalten)

Ich werde die Ableitung von überspringen Y und zum Verständnis gehen Y. Mike Vainers Beitrag enthält viele Details.

Die Form von Y

Yist definiert als (im Lambda-Kalkül- Format):

Y f = λs.(f (s s)) λs.(f (s s))

Wenn wir die Variable slinks von den Funktionen ersetzen , erhalten wir

Y f = λs.(f (s s)) λs.(f (s s))
=> f (λs.(f (s s)) λs.(f (s s)))
=> f (Y f)

Das Ergebnis von (Y f)ist in der Tat der Fixpunkt von f.

Warum (Y f)arbeitet?

Abhängig von der Signatur von f, (Y f)kann eine Funktion jeder Arität sein. Nehmen wir zur Vereinfachung an, dass (Y f)nur ein Parameter verwendet wird, wie unsere Fakultätsfunktion.

def fn fr x = accumulate x (fr (- x 1))

da machen fn fr = frwir weiter

=> accumulate x (fn fr (- x 1))
=> accumulate x (accumulate (- x 1) (fr (- x 2)))
=> accumulate x (accumulate (- x 1) (accumulate (- x 2) ... (fn fr 1)))

Die rekursive Berechnung wird beendet, wenn das Innerste (fn fr 1)der Basisfall ist und fnnicht frin der Berechnung verwendet wird.

Nochmal Yanschauen:

fr = Y fn = λs.(fn (s s)) λs.(fn (s s))
=> fn (λs.(fn (s s)) λs.(fn (s s)))

Damit

fr x = Y fn x = fn (λs.(fn (s s)) λs.(fn (s s))) x

Für mich sind die magischen Teile dieses Setups:

  • fnund frvoneinander abhängig: fr'Wraps' im fnInneren, jedes Mal, wenn fres zum Berechnen verwendet wird x, 'spawnt' es ('hebt'?) fnund delegiert die Berechnung an dieses fn(in sich selbst frund x). Auf der anderen Seite fnhängt davon ab frund verwendet fr, um das Ergebnis eines kleineren Problems zu berechnen x-1.
  • Zu der Zeit frwird verwendet , um zu definieren fn(bei fnVerwendungen frin seinen Operationen), die realen frsind noch nicht definiert.
  • Es ist das, fnwas die eigentliche Geschäftslogik definiert. Basierend auf fn, Yschafft fr- eine Hilfsfunktion in einer bestimmten Form - die Berechnung zu erleichtern fnin einer rekursiven Weise.

Es hat mir Yim Moment geholfen, diesen Weg zu verstehen , hoffe es hilft.

Übrigens fand ich das Buch Eine Einführung in die funktionale Programmierung durch Lambda-Kalkül auch sehr gut, ich bin nur ein Teil davon und die Tatsache, dass ich Ymich in dem Buch nicht zurechtfinden konnte, führte mich zu diesem Beitrag.


5

Hier finden Sie Antworten auf die ursprünglichen Fragen , die aus dem Artikel zusammengestellt wurden (der VOLLSTÄNDIG lesenswert ist), der in der Antwort von Nicholas Mancuso erwähnt wurde , sowie weitere Antworten:

Was ist ein Y-Kombinator?

Ein Y-Kombinator ist eine "Funktion" (oder eine Funktion höherer Ordnung - eine Funktion, die mit anderen Funktionen arbeitet), die ein einzelnes Argument verwendet, eine Funktion, die nicht rekursiv ist, und eine Version der Funktion zurückgibt rekursiv.


Etwas rekursiv =), aber detaillierter definiert:

Ein Kombinator - ist nur ein Lambda-Ausdruck ohne freie Variablen.
Freie Variable - ist eine Variable, die keine gebundene Variable ist.
Gebundene Variable - Variable, die im Hauptteil eines Lambda-Ausdrucks enthalten ist, dessen Variablenname eines seiner Argumente ist.

Eine andere Möglichkeit, darüber nachzudenken, besteht darin, dass der Kombinator ein solcher Lambda-Ausdruck ist, bei dem Sie den Namen eines Kombinators überall dort, wo er gefunden wird, durch seine Definition ersetzen können und alles noch funktioniert (Sie werden in eine Endlosschleife geraten, wenn der Kombinator dies tun würde Verweis auf sich selbst enthalten, innerhalb des Lambda-Körpers).

Der Y-Kombinator ist ein Festpunktkombinator.

Der Fixpunkt einer Funktion ist ein Element der Funktionsdomäne, das von der Funktion auf sich selbst abgebildet wird.
Das heißt, cist ein fester Punkt der Funktion, f(x)wenn f(c) = c
dies bedeutetf(f(...f(c)...)) = fn(c) = c

Wie funktionieren Kombinatoren?

Die folgenden Beispiele setzen eine starke + dynamische Typisierung voraus :

Fauler Y-Kombinator (normaler Ordnung):
Diese Definition gilt für Sprachen mit fauler (auch: verzögerter, bedarfsgerechter) Bewertung - Bewertungsstrategie, die die Bewertung eines Ausdrucks verzögert, bis sein Wert benötigt wird.

Y = λf.(λx.f(x x)) (λx.f(x x)) = λf.(λx.(x x)) (λx.f(x x))

Dies bedeutet, dass für eine gegebene Funktion f(die eine nicht rekursive Funktion ist) die entsprechende rekursive Funktion zuerst erhalten werden kann λx.f(x x), indem dieser Lambda-Ausdruck berechnet und dann auf sich selbst angewendet wird .

Strenger Y-Kombinator (Anwendungsreihenfolge):
Diese Definition gilt für Sprachen mit strenger (auch: eifriger, gieriger) Bewertung - Bewertungsstrategie, bei der ein Ausdruck bewertet wird, sobald er an eine Variable gebunden ist.

Y = λf.(λx.f(λy.((x x) y))) (λx.f(λy.((x x) y))) = λf.(λx.(x x)) (λx.f(λy.((x x) y)))

Es ist das gleiche wie faul in seiner Natur, es hat nur eine zusätzliche λHülle, um die Körperbewertung des Lambda zu verzögern. Ich habe eine andere Frage gestellt , die etwas mit diesem Thema zu tun hat.

Wofür sind sie gut?

Stolen entlehnt Antwort von Chris Ammerman : Y-Combinator generalizes Rekursion, deren Umsetzungabstrahieren, und damit es von der eigentlichen Arbeit der Funktion in Fragetrennen.

Obwohl der Y-Kombinator einige praktische Anwendungen hat, handelt es sich hauptsächlich um ein theoretisches Konzept, dessen Verständnis Ihre Gesamtvision erweitert und wahrscheinlich Ihre Analyse- und Entwicklerfähigkeiten verbessert.

Sind sie in prozeduralen Sprachen nützlich?

Wie von Mike Vanier angegeben : Es ist möglich, einen Y-Kombinator in vielen statisch typisierten Sprachen zu definieren, aber (zumindest in den Beispielen, die ich gesehen habe) erfordern solche Definitionen normalerweise einen nicht offensichtlichen Typ-Hackery, da der Y-Kombinator selbst dies nicht tut. t haben einen einfachen statischen Typ. Das geht über den Rahmen dieses Artikels hinaus, daher werde ich es nicht weiter erwähnen

Und wie von Chris Ammerman erwähnt : Die meisten prozeduralen Sprachen haben statische Typisierung.

Also antworte auf diese Frage - nicht wirklich.


4

Der y-Kombinator implementiert eine anonyme Rekursion. Also statt

function fib( n ){ if( n<=1 ) return n; else return fib(n-1)+fib(n-2) }

du kannst tun

function ( fib, n ){ if( n<=1 ) return n; else return fib(n-1)+fib(n-2) }

Natürlich funktioniert der y-Kombinator nur in Call-by-Name-Sprachen. Wenn Sie dies in einer normalen Call-by-Value-Sprache verwenden möchten, benötigen Sie den zugehörigen Z-Kombinator (der Y-Kombinator divergiert / Endlosschleife).


Der Y-Kombinator kann mit Wertübergabe und verzögerter Auswertung arbeiten.
Quelklef

3

Ein Festkomma-Kombinator (oder Festkomma-Operator) ist eine Funktion höherer Ordnung, die einen Festpunkt anderer Funktionen berechnet. Diese Operation ist in der Programmiersprachentheorie relevant, da sie die Implementierung einer Rekursion in Form einer Umschreiberegel ohne explizite Unterstützung durch die Laufzeit-Engine der Sprache ermöglicht. (src Wikipedia)


3

Der this-Operator kann Ihr Leben vereinfachen:

var Y = function(f) {
    return (function(g) {
        return g(g);
    })(function(h) {
        return function() {
            return f.apply(h(h), arguments);
        };
    });
};

Dann vermeiden Sie die Zusatzfunktion:

var fac = Y(function(n) {
    return n == 0 ? 1 : n * this(n - 1);
});

Schließlich rufen Sie an fac(5).


0

Ich denke, der beste Weg, dies zu beantworten, ist die Auswahl einer Sprache wie JavaScript:

function factorial(num)
{
    // If the number is less than 0, reject it.
    if (num < 0) {
        return -1;
    }
    // If the number is 0, its factorial is 1.
    else if (num == 0) {
        return 1;
    }
    // Otherwise, call this recursive procedure again.
    else {
        return (num * factorial(num - 1));
    }
}

Schreiben Sie es jetzt so um, dass es nicht den Namen der Funktion innerhalb der Funktion verwendet, sondern es dennoch rekursiv aufruft.

Der einzige Ort, an dem der Funktionsname angezeigt werden factorialsollte, ist die Aufrufstelle.

Hinweis: Sie können keine Namen von Funktionen verwenden, aber Sie können Namen von Parametern verwenden.

Arbeiten Sie das Problem. Schau nicht nach. Sobald Sie es gelöst haben, werden Sie verstehen, welches Problem der y-Kombinator löst.


1
Sind Sie sicher, dass es nicht mehr Probleme verursacht als löst?
Noctis Skytower

1
Noctis, können Sie Ihre Frage klären? Fragen Sie sich, ob das Konzept eines y-Kombinators selbst mehr Probleme verursacht als löst, oder sprechen Sie speziell darüber, dass ich mich dazu entschlossen habe, insbesondere JavaScript zu verwenden, oder über meine spezifische Implementierung oder meine Empfehlung, es zu lernen, indem ich es selbst als entdeckte Ich beschrieb?
zumalifeguard
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.