Che cos'è un combinatore a Y? [chiuso]


392

Un Y-combinatore è un concetto di informatica dal lato "funzionale" delle cose. La maggior parte dei programmatori non sa molto dei combinatori, se ne ha persino sentito parlare.

  • Che cos'è un combinatore a Y?
  • Come funzionano i combinatori?
  • A cosa servono?
  • Sono utili nei linguaggi procedurali?

12
Un piccolo suggerimento, se stai imparando i linguaggi funzionali come me, meglio lasciare i combinatori fino a quando non ti senti a tuo agio, altrimenti è una strada per la follia ...
Igor Zevaka

3
Devo sorridere al gravatar dell'editor di questa domanda :) Link correlato sul blog di Mads Torgensen
Benjol,


1
Ho scritto un breve riassunto della mia comprensione del Combinatore Y: gist.github.com/houtianze/b274e4b975a28fe08aee681699c3f7d0 Ho spiegato (per quanto ne so) come "il Combinatore Y fa funzione ricorsiva"
ibic

1
In che modo questa domanda è "troppo ampia"?
Rei Miyasaka,

Risposte:


201

Se sei pronto per una lunga lettura, Mike Vanier ha una grande spiegazione . Per farla breve, ti consente di implementare la ricorsione in un linguaggio che non lo supporta necessariamente in modo nativo.


14
Tuttavia è leggermente più di un collegamento; è un collegamento con un sommario molto breve . Un sommario più lungo sarebbe apprezzato.
Martijn Pieters

2
È solo un link MA non può andare meglio di così. Questa risposta merita (aggiungi 1 voti) senza alcuna condizione di caso base per uscire; aka ricorsione infinita.
Yavar,

7
@Andre MacFie: non ho commentato lo sforzo, ho commentato la qualità. In generale, la politica su Stack Overflow prevede che le risposte debbano essere autonome, con collegamenti a ulteriori informazioni.
Jørgen Fogh,

1
@galdre ha ragione. È un ottimo collegamento, ma è solo un collegamento. È stato anche menzionato in altre 3 risposte di seguito, ma solo come documento giustificativo in quanto tutte buone spiegazioni per conto proprio. Anche questa risposta non tenta nemmeno di rispondere alle domande del PO.
Toraritte,

290

Un Y-combinatore è un "funzionale" (una funzione che opera su altre funzioni) che consente la ricorsione, quando non è possibile fare riferimento alla funzione dall'interno di se stesso. Nella teoria dell'informatica, generalizza la ricorsione , astrattandone l'implementazione e quindi separandola dal lavoro effettivo della funzione in questione. Il vantaggio di non aver bisogno di un nome in fase di compilazione per la funzione ricorsiva è una sorta di bonus. =)

Questo è applicabile in lingue che supportano le funzioni lambda . La natura basata sull'espressione degli lambda di solito significa che non possono riferirsi a se stessi per nome. E aggirare questo problema dichiarando la variabile, riferendosi ad essa, quindi assegnando ad essa la lambda, per completare il ciclo di autoreferenziazione, è fragile. La variabile lambda può essere copiata e la variabile originale riassegnata, interrompendo l'autoreferenzialità.

I combinatori di Y sono ingombranti da implementare e spesso da usare in linguaggi di tipo statico (che spesso sono i linguaggi procedurali ), poiché in genere le restrizioni di digitazione richiedono che il numero di argomenti per la funzione in questione sia noto al momento della compilazione. Ciò significa che un combinatore-y deve essere scritto per qualsiasi conteggio degli argomenti che uno deve usare.

Di seguito è riportato un esempio di come l'utilizzo e il funzionamento di un Y-Combinator, in C #.

L'uso di un Y-combinatore implica un modo "insolito" di costruire una funzione ricorsiva. Per prima cosa devi scrivere la tua funzione come un pezzo di codice che chiama una funzione preesistente, piuttosto che se stessa:

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

Quindi lo trasformi in una funzione che richiede una funzione da chiamare e restituisce una funzione che lo fa. Questo si chiama funzionale, perché richiede una funzione ed esegue un'operazione con essa che risulta in un'altra funzione.

// 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);

Ora hai una funzione che accetta una funzione e restituisce un'altra funzione che sembra un fattoriale, ma invece di chiamare se stessa, chiama l'argomento passato nella funzione esterna. Come si fa a rendere fattoriale? Passa la funzione interna a se stessa. Lo Y-Combinator lo fa, essendo una funzione con un nome permanente, che può introdurre la ricorsione.

// 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.
}

Invece della chiamata fattoriale stessa, ciò che accade è che il fattoriale chiama il generatore fattoriale (restituito dalla chiamata ricorsiva a Y-Combinator). E a seconda del valore corrente di t la funzione restituita dal generatore richiamerà nuovamente il generatore, con t - 1, o semplicemente restituirà 1, terminando la ricorsione.

È complicato e criptico, ma tutto si esaurisce in fase di esecuzione e la chiave del suo funzionamento è "esecuzione differita" e la rottura della ricorsione per estendere due funzioni. La F interna viene passata come argomento , da chiamare nella prossima iterazione, solo se necessario .


5
Perché oh perché hai dovuto chiamarlo 'Y' e il parametro 'F'! Si perdono solo negli argomenti di tipo!
Brian Henk,

3
In Haskell, puoi ricorrere all'astrazione con:, fix :: (a -> a) -> ae la alattina a sua volta può essere una funzione di tutti gli argomenti che desideri. Ciò significa che la digitazione statica non rende davvero ingombrante.
Peaker,

12
Secondo la descrizione di Mike Vanier, la tua definizione di Y è in realtà non è un combinatore perché è ricorsivo. Sotto "Eliminare (la maggior parte) ricorsione esplicita (versione pigra)" ha lo schema pigro equivalente del tuo codice C # ma spiega al punto 2: "Non è un combinatore, perché la Y nel corpo della definizione è una variabile libera che viene associato solo una volta che la definizione è completa ... "Penso che il bello degli combinatori Y sia che producono ricorsione valutando il punto fisso di una funzione. In questo modo, non hanno bisogno di ricorsioni esplicite.
Concedi il

@GrantJ Hai ragione. Sono passati un paio d'anni da quando ho pubblicato questa risposta. Scremando il post di Vanier ora vedo che ho scritto Y, ma non un Y-Combinator. Leggerò presto il suo post e vedrò se posso pubblicare una correzione. Il mio istinto mi sta avvertendo che la tipizzazione statica rigorosa di C # potrebbe impedirlo alla fine, ma vedrò cosa posso fare.
Chris Ammerman,

1
@WayneBurkett È una pratica abbastanza comune in matematica.
YoTengoUnLCD,

102

Ho sollevato questo da http://www.mail-archive.com/boston-pm@mail.pm.org/msg02716.html che è una spiegazione che ho scritto diversi anni fa.

In questo esempio userò JavaScript, ma funzioneranno anche molte altre lingue.

Il nostro obiettivo è essere in grado di scrivere una funzione ricorsiva di 1 variabile usando solo le funzioni di 1 variabile e nessuna assegnazione, definendo le cose per nome, ecc. (Perché questo è il nostro obiettivo è un'altra domanda, prendiamo semplicemente questa come la sfida che ti viene dato.) Sembra impossibile, eh? Ad esempio, implementiamo fattoriale.

Bene, il primo passo è dire che potremmo farlo facilmente se avessimo barato un po '. Usando le funzioni di 2 variabili e assegnazione possiamo almeno evitare di usare l'assegnazione per impostare la ricorsione.

// 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
);

Ora vediamo se possiamo imbrogliare di meno. Bene, in primo luogo stiamo usando il compito, ma non è necessario. Possiamo semplicemente scrivere X e Y in linea.

// 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
);

Ma stiamo usando le funzioni di 2 variabili per ottenere una funzione di 1 variabile. Possiamo sistemarlo? Beh, un ragazzo intelligente di nome Haskell Curry ha un trucco, se hai buone funzioni di ordine superiore allora hai solo bisogno di funzioni di 1 variabile. La prova è che puoi ottenere dalle funzioni di 2 (o più nel caso generale) a 1 variabile con una trasformazione del testo puramente meccanica come questa:

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

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

dove ... rimane esattamente lo stesso. (Questo trucco si chiama "curry" dal nome del suo inventore. Il linguaggio Haskell è anche chiamato per Haskell Curry. File che sotto banalità inutili.) Ora applichiamo questa trasformazione ovunque e otteniamo la nostra versione finale.

// 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
);

Sentiti libero di provarlo. alert () che ritorna, legalo a un pulsante, qualunque cosa. Tale codice calcola i fattoriali, in modo ricorsivo, senza utilizzare assegnazioni, dichiarazioni o funzioni di 2 variabili. (Ma cercare di rintracciare il modo in cui funziona probabilmente farà girare la testa. E passarlo, senza derivazione, solo leggermente riformattato comporterà un codice che sicuramente confonderà e confonderà.)

È possibile sostituire le 4 righe che definiscono ricorsivamente fattoriale con qualsiasi altra funzione ricorsiva desiderata.


Bella spiegazione. Perché hai scritto function (n) { return builder(builder)(n);}invece di builder(builder)?
v7d8dpo4,

@ v7d8dpo4 Perché stavo trasformando una funzione di 2 variabili in una funzione di ordine superiore di una variabile usando il curry.
btilly

È questo il motivo per cui abbiamo bisogno di chiusure?
TheChetan

1
@TheChetan Closures ci consente di associare comportamenti personalizzati dietro una chiamata a una funzione anonima. È solo un'altra tecnica di astrazione.
btilly

85

Mi chiedo se sia utile tentare di costruirlo da zero. Vediamo. Ecco una funzione fattoriale di base, ricorsiva:

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

Rifattorizziamo e creiamo una nuova funzione chiamata factche restituisce una funzione di calcolo fattoriale anonima invece di eseguire il calcolo stesso:

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

var factorial = fact();

È un po 'strano, ma non c'è niente di sbagliato in questo. Stiamo solo generando una nuova funzione fattoriale ad ogni passaggio.

La ricorsione in questa fase è ancora abbastanza esplicita. La factfunzione deve essere consapevole del proprio nome. Parametrizziamo la chiamata ricorsiva:

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);

È fantastico, ma recurserdeve ancora conoscere il proprio nome. Parametrizziamo anche quello:

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

var factorial = recurser(recurser);

Ora, invece di chiamare recurser(recurser)direttamente, creiamo una funzione wrapper che restituisce il suo risultato:

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

var factorial = Y();

Ora possiamo eliminare del recursertutto il nome; è solo un argomento per la funzione interna di Y, che può essere sostituita con la funzione stessa:

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

var factorial = Y();

L'unico nome esterno a cui si fa ancora riferimento è fact, ma ormai dovrebbe essere chiaro che anche questo è facilmente parametrizzabile, creando la soluzione completa, generica:

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);
    };
});

Una spiegazione simile in JavaScript: igstan.ro/posts/…
Pops

1
Mi hai perso quando hai introdotto la funzione recurser. Non ho la minima idea di cosa stia facendo o perché.
Mörre,

2
Stiamo cercando di creare una soluzione ricorsiva generica per funzioni che non sono esplicitamente ricorsive. La recurserfunzione è il primo passo verso questo obiettivo, perché ci fornisce una versione ricorsiva factche non fa mai riferimento a se stessa per nome.
Wayne,

@WayneBurkett, posso riscrivere il combinatore Y in questo modo: function Y(recurse) { return recurse(recurse); } let factorial = Y(creator => value => { return value == 0 ? 1 : value * creator(creator)(value - 1); });. Ed è così che lo digerisco (non sono sicuro che sia corretto): non facendo esplicitamente riferimento alla funzione (non consentita come combinatore ), possiamo usare due funzioni parzialmente applicate / curry (una funzione creatore e la funzione di calcolo), con quale possiamo creare funzioni lambda / anonime che ottengono ricorsive senza bisogno di un nome per la funzione di calcolo?
Neevek,

50

La maggior parte delle risposte di cui sopra descrivono ciò che l'Y-Combinator è , ma non è quello che è per .

I combinatori a punto fisso vengono utilizzati per mostrare che il calcolo lambda è completo . Questo è un risultato molto importante nella teoria del calcolo e fornisce una base teorica per la programmazione funzionale .

Lo studio dei combinatori a virgola fissa mi ha anche aiutato a capire davvero la programmazione funzionale. Non ho mai trovato alcun uso per loro nella programmazione reale però.


24

combinatore y 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

Modifica : imparo molto guardando il codice, ma questo è un po 'difficile da ingoiare senza un po' di background - mi dispiace per quello. Con alcune conoscenze generali presentate da altre risposte, puoi iniziare a distinguere ciò che sta accadendo.

La funzione Y è il "combinatore y". Ora dai un'occhiata alla var factoriallinea in cui viene utilizzato Y. Si noti che si passa a una funzione che ha un parametro (in questo esempio recurse) che verrà utilizzato anche in seguito nella funzione interna. Il nome del parametro diventa sostanzialmente il nome della funzione interna che gli consente di eseguire una chiamata ricorsiva (poiché utilizza recurse()nella sua definizione.) Il y-combinatore esegue la magia di associare la funzione interna altrimenti anonima al nome del parametro della funzione passato a Y.

Per la spiegazione completa di come Y fa la magia, controlla l' articolo collegato (non da me a proposito).


6
Javascript non ha bisogno di un Y-combinatore per eseguire la ricorsione anonima perché puoi accedere alla funzione corrente con argument.callee (vedi en.wikipedia.org/wiki/… )
xitrium

6
arguments.calleenon è consentito in modalità Strict: developer.mozilla.org/en/JavaScript/…
dave1010

2
Puoi comunque assegnare un nome a qualsiasi funzione e, se è espressione di funzione, quel nome è noto solo all'interno della funzione stessa. (function fact(n){ return n <= 1? 1 : n * fact(n-1); })(5)
Esailija,


18

Per i programmatori che non hanno approfondito la programmazione funzionale e non si preoccupano di iniziare ora, ma sono leggermente curiosi:

Il combinatore Y è una formula che consente di implementare la ricorsione in una situazione in cui le funzioni non possono avere nomi ma possono essere passate come argomenti, utilizzate come valori di ritorno e definite all'interno di altre funzioni.

Funziona passando la funzione a se stesso come argomento, quindi può chiamarsi.

Fa parte del calcolo lambda, che in realtà è matematica ma è in realtà un linguaggio di programmazione ed è piuttosto fondamentale per l'informatica e in particolare per la programmazione funzionale.

Il valore pratico quotidiano del combinatore Y è limitato, poiché i linguaggi di programmazione tendono a consentire di assegnare un nome alle funzioni.

Nel caso in cui sia necessario identificarlo in una formazione di polizia, è simile al seguente:

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

Di solito puoi individuarlo a causa del ripetuto (λx.f (x x)).

I λsimboli sono la lettera greca lambda, che dà il nome al calcolo lambda, e ci sono molti (λx.t)termini di stile perché è come appare il calcolo lambda.


questa dovrebbe essere la risposta accettata. A proposito, con U x = x x, Y = U . (. U)(abusando della notazione simile a Haskell). IOW, con combinatori adeguati, Y = BU(CBU). Così, Yf = U (f . U) = (f . U) (f . U) = f (U (f . U)) = f ((f . U) (f . U)).
Will Ness,

13

Ricorsione anonima

Un combinatore a punto fisso è una funzione di ordine superiore fixche per definizione soddisfa l'equivalenza

forall f.  fix f  =  f (fix f)

fix frappresenta una soluzione xall'equazione del punto fisso

               x  =  f x

Il fattoriale di un numero naturale può essere dimostrato da

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

L'uso di fixprove costruttive arbitrarie sulle funzioni generali / μ ricorsive può essere derivato senza autoreferenzialità non mistica.

fact n = (fix fact') n

dove

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

tale che

   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

Questa prova formale che

fact 3  =  6

utilizza metodicamente l'equivalenza del combinatore in virgola fissa per le riscritture

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

Calcolo lambda

Il formalismo del calcolo lambda non tipizzato consiste in una grammatica senza contesto

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

dove vva oltre le variabili, insieme alle regole di riduzione beta ed eta

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

La riduzione beta sostituisce tutte le occorrenze libere della variabile xnel corpo dell'astrazione ("funzione") Bcon l'espressione ("argomento") E. La riduzione dell'ETA elimina l'astrazione ridondante. A volte viene omesso dal formalismo. Un'espressione irriducibile , alla quale non si applica alcuna regola di riduzione, è in forma normale o canonica .

λ x y. E

è una scorciatoia per

λ x. λ y. E

(astrazione multiarità),

E F G

è una scorciatoia per

(E F) G

(associazione sinistra associatività),

λ x. x

e

λ y. y

sono equivalenti alfa .

Astrazione e applicazione sono gli unici due "primitivi linguistici" del calcolo lambda, ma consentono la codifica di dati e operazioni arbitrariamente complessi.

I numeri della Chiesa sono una codifica dei numeri naturali simili ai naturali Peano-assiomatici.

   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
    . . .

Una prova formale che

1 + 2  =  3

usando la regola di riscrittura della riduzione beta:

   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

combinatori

Nel calcolo lambda, i combinatori sono astrazioni che non contengono variabili libere. Più semplicemente: Iil combinatore di identità

λ x. x

isomorfo alla funzione identità

id x = x

Tali combinatori sono gli operatori primitivi dei calcoli combinatori come il sistema SKI.

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

La riduzione della beta non si sta fortemente normalizzando ; non tutte le espressioni riducibili, "redexes", convergono in forma normale sotto la riduzione beta. Un semplice esempio è l'applicazione divergente del ωcombinatore omega

λ x. x x

a se stesso:

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

Viene data la priorità alla riduzione delle sottoespressioni più a sinistra ("teste"). L'ordine applicabile normalizza gli argomenti prima della sostituzione, l'ordine normale no. Le due strategie sono analoghe alla valutazione avida, ad esempio C, e alla valutazione pigra, ad esempio Haskell.

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

diverge sotto l'aspra riduzione beta dell'ordine applicativo

=  (λ 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 quando in semantica rigorosa

forall f.  f _|_  =  _|_

ma converge sotto una pigra riduzione della beta di ordine normale

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

Se un'espressione ha una forma normale, la riduzione beta dell'ordine normale la troverà.

Y

La proprietà essenziale del Y combinatore a punto fisso

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

è dato da

   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))
. . .                                      . . .

L'equivalenza

Y g  =  g (Y g)

è isomorfo a

fix f  =  f (fix f)

Il calcolo lambda non tipizzato può codificare prove costruttive arbitrarie su funzioni generali / μ ricorsive.

 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

(Moltiplicazione ritardata, confluenza)

Per il calcolo lambda non tipizzato della Chiesa, inoltre, è stato dimostrato che esiste un infinito ricorsivamente enumerabile di combinatori a punto fisso 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))
  . . .

La riduzione beta dell'ordine normale rende il calcolo lambda non tipizzato non esteso un sistema di riscrittura completo di Turing.

In Haskell, il combinatore a punto fisso può essere implementato con eleganza

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

La pigrizia di Haskell si normalizza fino alla fine prima che tutte le sottoespressioni siano state valutate.

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


4
Anche se apprezzo la completezza della risposta, non è in alcun modo accessibile a un programmatore con pochi background matematici formali dopo l'interruzione della prima riga.
Jared Smith,

4
@ jared-smith La risposta ha lo scopo di raccontare una storia Wonkaiana supplementare sulle nozioni di CS / matematica dietro il combinatore Y. Penso che, probabilmente, le migliori analogie possibili a concetti familiari siano già state disegnate da altri risponditori. Personalmente, mi è sempre piaciuto confrontarmi con la vera origine, la radicale novità di un'idea, su una bella analogia. Trovo le analogie più ampie inopportune e confuse.

1
Ciao, combinatore di identità λ x . x, come stai oggi?
MaiaVictor

Mi piace questa risposta il più . Ha appena cancellato tutte le mie domande!
Studente

11

Altre risposte forniscono una risposta piuttosto concisa a questo, senza un fatto importante: non è necessario implementare un combinatore a virgola fissa in nessun linguaggio pratico in questo modo contorto e farlo non ha alcuno scopo pratico (tranne "guarda, so quale combinatore a Y è"). È un concetto teorico importante, ma di scarso valore pratico.


6

Ecco un'implementazione JavaScript di Y-Combinator e la funzione Factorial (dall'articolo di Douglas Crockford, disponibile su: 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

Un Y-Combinator è un altro nome per un condensatore di flusso.


4
Molto divertente. :) i giovani (er) potrebbero non riconoscere il riferimento però.
Will Ness,

2
haha! Sì, il giovane (io) riesco ancora a capire ...


Penso che questa risposta possa essere particolarmente confusa per chi non parla inglese. Si potrebbe dedicare un bel po 'di tempo a comprendere questa affermazione prima (o mai) di rendersi conto che si tratta di un riferimento umoristico alla cultura popolare. (Mi piace, mi sentirei solo male se avessi risposto a questa domanda e scoperto che qualcuno lo stava scoraggiando)
Mike

5

Ho scritto una sorta di "guida degli idioti" allo Y-Combinator sia in Clojure che in Scheme, al fine di aiutarmi a venire a patti. Sono influenzati dal materiale di "The Little Schemer"

Nello schema: https://gist.github.com/z5h/238891

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

Entrambi i tutorial sono intervallati da commenti e devono essere tagliati e sfogliabili nel tuo editor preferito.


5

Come principiante dei combinatori, ho trovato l'articolo di Mike Vanier (grazie a Nicholas Mancuso) davvero utile. Vorrei scrivere un riassunto, oltre a documentare la mia comprensione, se potesse essere di aiuto ad altri sarei molto contento.

Da Crappy a Meno Crappy

Usando fattoriale come esempio, usiamo la seguente almost-factorialfunzione per calcolare fattoriale di numero x:

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

Nello pseudo-codice sopra riportato, almost-factorialaccetta la funzione fe il numero x( almost-factorialè un curry, quindi può essere visto come prendere in funzione fe restituire una funzione 1-arity).

Quando almost-factorialcalcola fattoriale per x, delega il calcolo di fattoriale per x - 1funzionare fe accumula quel risultato con x(in questo caso, moltiplica il risultato di (x - 1) con x).

Può essere visto come almost-factorialassume una versione scadente della funzione fattoriale (che può calcolare solo fino a un numero x - 1) e restituisce una versione meno scadente di fattoriale (che calcola fino a un numero x). Come in questo modulo:

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

Se passiamo ripetutamente la versione meno scadente di fattoriale almost-factorial, alla fine otterremo la funzione fattoriale desiderata f. Dove può essere considerato come:

almost-factorial f = f

Fix-point

Il fatto che almost-factorial f = fsignifica fè il punto fisso della funzione almost-factorial.

Questo è stato un modo davvero interessante di vedere le relazioni delle funzioni sopra ed è stato un momento per me. (per favore leggi il post di Mike sul punto fisso se non l'hai fatto)

Tre funzioni

Per generalizzare, abbiamo un non-ricorsiva funzione fn(come il nostro quasi-fattoriale), abbiamo la sua correzione punti funzione fr(come il nostro f), allora che cosa Yfa è quando si dà Y fn, Yrestituisce la funzione di correzione-punto di fn.

Quindi in sintesi (semplificato assumendo frprende solo un parametro; xdegenera in x - 1, x - 2... in ricorsione):

  • Definiamo i calcoli di base come fn: def fn fr x = ...accumulate x with result from (fr (- x 1))questa è la quasi-utile funzione - anche se non possiamo usare fndirettamente x, sarà utile molto presto. Questo non ricorsivo fnutilizza una funzione frper calcolare il suo risultato
  • fn fr = fr, frÈ la correzione-punto fn, frè l' utile funciton, possiamo usare frsu xper ottenere il nostro risultato
  • Y fn = fr, YRestituisce il fix-point di una funzione, Y si trasforma il nostro quasi-utile funzione fnin utile fr

Derivazione Y(non inclusa)

Salterò la derivazione di Ye andrò a capire Y. Il post di Mike Vainer ha molti dettagli.

La forma di Y

Yè definito come (nel formato di calcolo lambda ):

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

Se sostituiamo la variabile sa sinistra delle funzioni, otteniamo

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

Quindi, in effetti, il risultato (Y f)è il punto fisso di f.

Perché (Y f)funziona

A seconda della firma di f, (Y f)può essere una funzione di qualsiasi arità, per semplificare, supponiamo (Y f)che prenda solo un parametro, come la nostra funzione fattoriale.

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

da allora fn fr = fr, continuiamo

=> 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)))

il calcolo ricorsivo termina quando il più interno (fn fr 1)è il caso base e fnnon viene utilizzato frnel calcolo.

Guardando di Ynuovo:

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

Così

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

Per me, le parti magiche di questa configurazione sono:

  • fne frinterdipendono l'uno con l'altro: fr'avvolge' fndentro, ogni volta che frviene usato per calcolare x, 'spawn' ('ascensori'?) an fne delega il calcolo a quello fn(passando in sé fre x); d'altra parte, fndipende fre usa frper calcolare il risultato di un problema minore x-1.
  • Al momento frviene utilizzato per definire fn(quando fnutilizza frnelle sue operazioni), il reale frnon è ancora definito.
  • È ciò fnche definisce la vera logica aziendale. Basato su fn, Ycrea fr- una funzione di supporto in una forma specifica - per facilitare il calcolo fnin modo ricorsivo .

Mi ha aiutato a capire in Yquesto modo al momento, spero che aiuti.

A proposito, ho anche trovato molto buono il libro An Introduction to Functional Programming Through Lambda Calculus , ci sono solo una parte e il fatto che non riuscissi a capire meglio Yil libro mi ha portato a questo post.


5

Ecco le risposte alle domande originali , compilate dall'articolo (che è TOTALMENTE degno di lettura) menzionato nella risposta di Nicholas Mancuso , così come altre risposte:

Che cos'è un combinatore a Y?

Un Y-combinatore è un "funzionale" (o una funzione di ordine superiore - una funzione che opera su altre funzioni) che accetta un singolo argomento, che è una funzione non ricorsiva, e restituisce una versione della funzione che è ricorsivo.


Un po 'ricorsivo =), ma una definizione più approfondita:

Un combinatore - è solo un'espressione lambda senza variabili libere.
Variabile libera: è una variabile che non è una variabile associata.
Variabile associata: variabile contenuta nel corpo di un'espressione lambda che ha il nome di quella variabile come uno dei suoi argomenti.

Un altro modo di pensare a questo è che il combinatore è una tale espressione lambda, in cui sei in grado di sostituire il nome di un combinatore con la sua definizione ovunque sia trovato e avere tutto ancora funzionante (entrerai in un ciclo infinito se il combinatore lo farebbe contiene riferimenti a se stesso, all'interno del corpo lambda).

Il combinatore Y è un combinatore a punto fisso.

Il punto fisso di una funzione è un elemento del dominio della funzione che è mappato a se stesso dalla funzione.
Vale a dire, cè un punto fisso della funzione f(x)se f(c) = c
questo significaf(f(...f(c)...)) = fn(c) = c

Come funzionano i combinatori?

Gli esempi seguenti assumono una digitazione forte + dinamica :

Combinatore Y pigro (ordine normale):
questa definizione si applica alle lingue con valutazione pigra (anche: differita, chiamata per necessità) - strategia di valutazione che ritarda la valutazione di un'espressione fino a quando non è necessario il suo valore.

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

Ciò significa che, per una determinata funzione f(che è una funzione non ricorsiva), la funzione ricorsiva corrispondente può essere ottenuta prima calcolando λx.f(x x), quindi applicando questa espressione lambda a se stessa.

Combinatore Y rigoroso (ordine applicativo):
questa definizione si applica alle lingue con valutazione rigorosa (anche: avida, avida) - strategia di valutazione in cui un'espressione viene valutata non appena è legata a una variabile.

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

È uguale a quello pigro nella sua natura, ha solo un λinvolucro extra per ritardare la valutazione del corpo della lambda. Ho fatto un'altra domanda , in qualche modo correlata a questo argomento.

A cosa servono?

Rubato dalla risposta di Chris Ammerman : il combinatore Y generalizza la ricorsione, ne astratta la sua attuazione e quindi la separa dal lavoro effettivo della funzione in questione.

Anche se Y-combinator ha alcune applicazioni pratiche, è principalmente un concetto teorico, la cui comprensione amplierà la tua visione generale e, probabilmente, aumenterà le tue capacità analitiche e di sviluppo.

Sono utili nei linguaggi procedurali?

Come affermato da Mike Vanier : è possibile definire un combinatore Y in molti linguaggi tipicamente statici, ma (almeno negli esempi che ho visto) tali definizioni di solito richiedono un hacker di tipo non ovvio, perché lo stesso combinatore Y non lo fa ' hanno un tipo statico semplice. Questo va oltre lo scopo di questo articolo, quindi non lo menzionerò ulteriormente

E come menzionato da Chris Ammerman : la maggior parte dei linguaggi procedurali ha una tipizzazione statica.

Quindi rispondi a questo - non proprio.


4

Il combinatore y implementa la ricorsione anonima. Quindi invece di

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

tu puoi fare

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

ovviamente, il combinatore y funziona solo nei linguaggi call-by-name. Se vuoi usarlo in qualsiasi normale linguaggio call-by-value, allora avrai bisogno del relativo combinatore z (il combinatore y divergerà / ciclo infinito).


Il combinatore Y può funzionare con valutazione pass-by-value e lazy.
Quelklef,

3

Un combinatore a punto fisso (o operatore a punto fisso) è una funzione di ordine superiore che calcola un punto fisso di altre funzioni. Questa operazione è rilevante nella teoria del linguaggio di programmazione perché consente l'implementazione della ricorsione sotto forma di una regola di riscrittura, senza supporto esplicito dal motore di runtime del linguaggio. (src Wikipedia)


3

Questo operatore può semplificarti la vita:

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

Quindi eviti la funzione extra:

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

Finalmente chiami fac(5).


0

Penso che il modo migliore per rispondere sia scegliere una lingua, come 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));
    }
}

Ora riscrivilo in modo che non utilizzi il nome della funzione all'interno della funzione, ma la chiami ancora in modo ricorsivo.

L'unico posto in cui si factorialdovrebbe vedere il nome della funzione è sul sito della chiamata.

Suggerimento: non è possibile utilizzare nomi di funzioni, ma è possibile utilizzare nomi di parametri.

Risolvi il problema. Non cercare. Una volta risolto, capirai quale problema risolve il combinatore y.


1
Sei sicuro che non crei più problemi di quanti ne risolva?
Noctis Skytower,

1
Noctis, puoi chiarire la tua domanda? Stai chiedendo se il concetto di un y-combinatore stesso crea più problemi di quanti ne risolva, o stai parlando in particolare che ho scelto di dimostrare usando JavaScript in particolare, o la mia implementazione specifica o la mia raccomandazione per impararlo scoprendolo tu stesso come Ho descritto?
zumalifeguard,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.