Puntatori a funzione, chiusure e Lambda


86

Sto solo imparando a conoscere i puntatori a funzione e, mentre leggevo il capitolo K&R sull'argomento, la prima cosa che mi ha colpito è stata: "Ehi, questa è una specie di chiusura". Sapevo che questa ipotesi era fondamentalmente sbagliata in qualche modo e dopo una ricerca online non ho trovato davvero alcuna analisi di questo confronto.

Allora perché i puntatori a funzione in stile C sono fondamentalmente diversi dalle chiusure o dai lambda? Per quanto ne so, ha a che fare con il fatto che il puntatore a funzione punta ancora a una funzione definita (denominata) invece della pratica di definire in modo anonimo la funzione.

Perché passare una funzione a una funzione vista come più potente nel secondo caso, in cui è senza nome, rispetto al primo in cui è solo una normale funzione quotidiana che viene passata?

Per favore dimmi come e perché sbaglio a confrontare i due così da vicino.

Grazie.

Risposte:


108

Un lambda (o chiusura ) incapsula sia il puntatore a funzione che le variabili. Questo è il motivo per cui, in C #, puoi fare:

int lessThan = 100;
Func<int, bool> lessThanTest = delegate(int i) {
   return i < lessThan;
};

Ho usato un delegato anonimo come chiusura (la sua sintassi è un po 'più chiara e più vicina a C rispetto all'equivalente lambda), che ha catturato lessThan (una variabile di stack) nella chiusura. Quando viene valutata la chiusura, si continuerà a fare riferimento a lessThan (il cui stack frame potrebbe essere stato distrutto). Se cambio meno di, cambio il confronto:

int lessThan = 100;
Func<int, bool> lessThanTest = delegate(int i) {
   return i < lessThan;
};

lessThanTest(99); // returns true
lessThan = 10;
lessThanTest(99); // returns false

In C, questo sarebbe illegale:

BOOL (*lessThanTest)(int);
int lessThan = 100;

lessThanTest = &LessThan;

BOOL LessThan(int i) {
   return i < lessThan; // compile error - lessThan is not in scope
}

anche se potrei definire un puntatore a funzione che accetta 2 argomenti:

int lessThan = 100;
BOOL (*lessThanTest)(int, int);

lessThanTest = &LessThan;
lessThanTest(99, lessThan); // returns true
lessThan = 10;
lessThanTest(100, lessThan); // returns false

BOOL LessThan(int i, int lessThan) {
   return i < lessThan;
}

Ma ora devo passare i 2 argomenti quando lo valuto. Se volessi passare questo puntatore a un'altra funzione in cui lessThan non era nell'ambito, dovrei mantenerlo attivo manualmente passandolo a ciascuna funzione nella catena o promuovendolo a globale.

Sebbene la maggior parte dei linguaggi tradizionali che supportano le chiusure utilizzino funzioni anonime, non è necessario farlo. Puoi avere chiusure senza funzioni anonime e funzioni anonime senza chiusure.

Riepilogo: una chiusura è una combinazione di puntatore a funzione + variabili catturate.


grazie, hai davvero portato a casa l'idea che altre persone dovevano cercare di arrivare.
Nessuno

Probabilmente stavi usando una versione precedente di C quando hai scritto questo o non ti sei ricordato di dichiarare in avanti la funzione, ma non osservo lo stesso comportamento che hai menzionato quando l'ho testato. ideone.com/JsDVBK
smac89

@ smac89 - hai reso globale la variabile lessThan - l'ho menzionato esplicitamente come alternativa.
Mark Brackett

42

Come qualcuno che ha scritto compilatori per lingue sia con che senza chiusure "reali", sono rispettosamente in disaccordo con alcune delle risposte sopra. Una chiusura Lisp, Scheme, ML o Haskell non crea dinamicamente una nuova funzione . Invece riutilizza una funzione esistente ma lo fa con nuove variabili libere . La raccolta di variabili libere è spesso chiamata ambiente , almeno dai teorici del linguaggio di programmazione.

Una chiusura è solo un aggregato contenente una funzione e un ambiente. Nel compilatore Standard ML del New Jersey, ne abbiamo rappresentato uno come record; un campo conteneva un puntatore al codice e gli altri campi contenevano i valori delle variabili libere. Il compilatore ha creato dinamicamente una nuova chiusura (non funzione) allocando un nuovo record contenente un puntatore allo stesso codice, ma con valori diversi per le variabili libere.

Puoi simulare tutto questo in C, ma è un rompicoglioni. Due tecniche sono popolari:

  1. Passa un puntatore alla funzione (il codice) e un puntatore separato alle variabili libere, in modo che la chiusura sia suddivisa in due variabili C.

  2. Passa un puntatore a una struttura, dove la struttura contiene i valori delle variabili libere e anche un puntatore al codice.

La tecnica n. 1 è ideale quando si cerca di simulare una sorta di polimorfismo in C e non si vuole rivelare il tipo di ambiente --- si usa un puntatore void * per rappresentare l'ambiente. Per esempi, guarda le interfacce C e le implementazioni di Dave Hanson . La tecnica n. 2, che assomiglia più da vicino a ciò che accade nei compilatori di codice nativo per linguaggi funzionali, assomiglia anche a un'altra tecnica familiare ... oggetti C ++ con funzioni membro virtuali. Le implementazioni sono quasi identiche.

Questa osservazione ha portato a una battuta di Henry Baker:

Le persone nel mondo Algol / Fortran si sono lamentate per anni di non aver capito quale possibile utilizzo avrebbero avuto le chiusure delle funzioni in una programmazione efficiente del futuro. Poi è avvenuta la rivoluzione della "programmazione orientata agli oggetti", e ora tutti programmano usando chiusure di funzioni, tranne per il fatto che continuano a rifiutarsi di chiamarle così.


1
+1 per la spiegazione e la citazione che OOP è davvero chiusure - riutilizza una funzione esistente ma lo fa con nuove variabili libere - funzioni (metodi) che prendono l'ambiente (un puntatore a struttura ai dati di istanza dell'oggetto che non è altro che nuovi stati) su cui operare.
legends2k

8

In C non puoi definire la funzione inline, quindi non puoi davvero creare una chiusura. Tutto quello che stai facendo è passare un riferimento a un metodo predefinito. Nelle lingue che supportano metodi / chiusure anonime, la definizione dei metodi è molto più flessibile.

In termini più semplici, ai puntatori a funzione non è associato alcun ambito (a meno che non si conti l'ambito globale), mentre le chiusure includono l'ambito del metodo che li definisce. Con lambda, puoi scrivere un metodo che scrive un metodo. Le chiusure consentono di associare "alcuni argomenti a una funzione e ottenere come risultato una funzione di valore minore". (tratto dal commento di Thomas). Non puoi farlo in C.

EDIT: Aggiungendo un esempio (userò la sintassi di Actionscript perché è quello che ho in mente in questo momento):

Supponi di avere un metodo che accetta un altro metodo come argomento, ma non fornisce un modo per passare alcun parametro a quel metodo quando viene chiamato? Come, diciamo, un metodo che causa un ritardo prima di eseguire il metodo che hai passato (esempio stupido, ma voglio mantenerlo semplice).

function runLater(f:Function):Void {
  sleep(100);
  f();
}

Ora supponi di voler all'utente runLater () per ritardare l'elaborazione di un oggetto:

function objectProcessor(o:Object):Void {
  /* Do something cool with the object! */
}

function process(o:Object):Void {
  runLater(function() { objectProcessor(o); });
}

La funzione che stai passando a process () non è più una funzione definita staticamente. Viene generato dinamicamente ed è in grado di includere riferimenti a variabili che erano nell'ambito quando il metodo è stato definito. Quindi, può accedere a "o" e "objectProcessor", anche se non sono nell'ambito globale.

Spero che abbia senso.


Ho modificato la mia risposta in base al tuo commento. Non sono ancora chiaro al 100% sulle specifiche dei termini, quindi ti ho appena citato direttamente. :)
Herms

L'abilità inline delle funzioni anonime è un dettaglio di implementazione dei (la maggior parte?) Dei linguaggi di programmazione tradizionali: non è un requisito per le chiusure.
Mark Brackett

6

Chiusura = logica + ambiente.

Ad esempio, considera questo metodo C # 3:

public Person FindPerson(IEnumerable<Person> people, string name)
{
    return people.Where(person => person.Name == name);
}

L'espressione lambda non solo incapsula la logica ("confronta il nome") ma anche l'ambiente, incluso il parametro (cioè la variabile locale) "nome".

Per ulteriori informazioni su questo, dai un'occhiata al mio articolo sulle chiusure che ti guida attraverso C # 1, 2 e 3, mostrando come le chiusure semplificano le cose.


valuta la possibilità di sostituire void con IEnumerable <Person>
Amy B

1
@David B: Salute, fatto. @edg: Penso che sia più di un semplice stato, perché è uno stato mutevole . In altre parole, se si esegue una chiusura che cambia una variabile locale (mentre si è ancora all'interno del metodo) anche quella variabile locale cambia. "Ambiente" sembra trasmetterlo meglio a me, ma è lanoso.
Jon Skeet

Apprezzo la risposta ma questo non mi chiarisce davvero nulla, sembra che le persone siano solo un oggetto e tu chiami un metodo su di esso. Forse è solo che non conosco C #.
Nessuno

Sì, sta chiamando un metodo su di esso, ma il parametro che sta passando è la chiusura.
Jon Skeet

4

In C, i puntatori a funzione possono essere passati come argomenti alle funzioni e restituiti come valori dalle funzioni, ma le funzioni esistono solo al livello più alto: non è possibile annidare le definizioni di funzione l'una nell'altra. Pensa a cosa ci vorrebbe per C per supportare funzioni annidate che possono accedere alle variabili della funzione esterna, pur essendo in grado di inviare puntatori a funzione su e giù per lo stack di chiamate. (Per seguire questa spiegazione, dovresti conoscere le basi di come le chiamate di funzione sono implementate in C e nella maggior parte dei linguaggi simili: sfoglia la voce dello stack di chiamate su Wikipedia.)

Che tipo di oggetto è un puntatore a una funzione annidata? Non può essere solo l'indirizzo del codice, perché se lo chiami, come accede alle variabili della funzione esterna? (Ricorda che a causa della ricorsione, possono esserci più chiamate differenti della funzione esterna attive contemporaneamente.) Questo è chiamato problema funarg , e ci sono due sottoproblemi: il problema funargs discendente e il problema funargs ascendente.

Il problema dei funarg discendenti, cioè l'invio di un puntatore a funzione "in basso nello stack" come argomento di una funzione che chiami, in realtà non è incompatibile con C, e GCC supporta le funzioni annidate come funarg discendenti. In GCC, quando crei un puntatore a una funzione nidificata, ottieni davvero un puntatore a un trampolino , un pezzo di codice costruito dinamicamente che imposta il puntatore al collegamento statico e quindi chiama la funzione reale, che utilizza il puntatore al collegamento statico per accedere le variabili della funzione esterna.

Il problema dei funargs verso l'alto è più difficile. GCC non ti impedisce di lasciare che esista un puntatore a trampolino dopo che la funzione esterna non è più attiva (non ha alcun record nello stack di chiamate) e quindi il puntatore al collegamento statico potrebbe puntare a spazzatura. I record di attivazione non possono più essere allocati in uno stack. La solita soluzione è di allocarli sull'heap e lasciare che un oggetto funzione che rappresenta una funzione annidata punti semplicemente al record di attivazione della funzione esterna. Un tale oggetto è chiamato chiusura . Quindi la lingua in genere dovrà supportare la garbage collection in modo che i record possano essere liberati una volta che non ci sono più puntatori che puntano a loro.

Lambdas ( funzioni anonime ) sono davvero una questione separata, ma di solito un linguaggio che ti consente di definire funzioni anonime al volo ti consente anche di restituirle come valori di funzione, quindi finiscono per essere chiusure.


3

Un lambda è un anonimo, definita dinamicamente . Non puoi farlo in C ... per quanto riguarda le chiusure (o la convinzione dei due), il tipico esempio lisp assomiglierebbe a qualcosa sulla falsariga di:

(defun get-counter (n-start +-number)
     "Returns a function that returns a number incremented
      by +-number every time it is called"
    (lambda () (setf n-start (+ +-number n-start))))

In termini C, si potrebbe dire che l'ambiente lessicale (lo stack) di get-counterviene catturato dalla funzione anonima e modificato internamente come mostra il seguente esempio:

[1]> (defun get-counter (n-start +-number)
         "Returns a function that returns a number incremented
          by +-number every time it is called"
        (lambda () (setf n-start (+ +-number n-start))))
GET-COUNTER
[2]> (defvar x (get-counter 2 3))
X
[3]> (funcall x)
5
[4]> (funcall x)
8
[5]> (funcall x)
11
[6]> (funcall x)
14
[7]> (funcall x)
17
[8]> (funcall x)
20
[9]> 

2

Le chiusure implicano che alcune variabili dal punto di definizione della funzione siano legate alla logica della funzione, come poter dichiarare un mini-oggetto al volo.

Un problema importante con C e le chiusure è che le variabili allocate nello stack verranno distrutte all'uscita dall'ambito corrente, indipendentemente dal fatto che una chiusura puntasse a loro. Questo porterebbe al tipo di bug che le persone ottengono quando restituiscono con noncuranza puntatori a variabili locali. Le chiusure implicano fondamentalmente che tutte le variabili rilevanti siano elementi conteggiati in riferimento o raccolti da rifiuti su un mucchio.

Non mi sento a mio agio nell'identificare lambda con la chiusura perché non sono sicuro che i lambda in tutte le lingue siano chiusure, a volte penso che i lambda siano stati solo funzioni anonime definite localmente senza l'associazione di variabili (Python pre 2.1?).


2

In GCC è possibile simulare funzioni lambda utilizzando la seguente macro:

#define lambda(l_ret_type, l_arguments, l_body)       \
({                                                    \
    l_ret_type l_anonymous_functions_name l_arguments \
    l_body                                            \
    &l_anonymous_functions_name;                      \
})

Esempio dalla fonte :

qsort (array, sizeof (array) / sizeof (array[0]), sizeof (array[0]),
     lambda (int, (const void *a, const void *b),
             {
               dump ();
               printf ("Comparison %d: %d and %d\n",
                       ++ comparison, *(const int *) a, *(const int *) b);
               return *(const int *) a - *(const int *) b;
             }));

L'uso di questa tecnica ovviamente rimuove la possibilità che la tua applicazione funzioni con altri compilatori ed è apparentemente un comportamento "indefinito", quindi YMMV.


2

La chiusura cattura le variabili libere in un ambiente . L'ambiente esisterà ancora, anche se il codice circostante potrebbe non essere più attivo.

Un esempio in Common Lisp, dove MAKE-ADDERrestituisce una nuova chiusura.

CL-USER 53 > (defun make-adder (start delta) (lambda () (incf start delta)))
MAKE-ADDER

CL-USER 54 > (compile *)
MAKE-ADDER
NIL
NIL

Utilizzando la funzione sopra:

CL-USER 55 > (let ((adder1 (make-adder 0 10))
                   (adder2 (make-adder 17 20)))
               (print (funcall adder1))
               (print (funcall adder1))
               (print (funcall adder1))
               (print (funcall adder1))
               (print (funcall adder2))
               (print (funcall adder2))
               (print (funcall adder2))
               (print (funcall adder1))
               (print (funcall adder1))
               (describe adder1)
               (describe adder2)
               (values))

10 
20 
30 
40 
37 
57 
77 
50 
60 
#<Closure 1 subfunction of MAKE-ADDER 4060001ED4> is a CLOSURE
Function         #<Function 1 subfunction of MAKE-ADDER 4060001CAC>
Environment      #(60 10)
#<Closure 1 subfunction of MAKE-ADDER 4060001EFC> is a CLOSURE
Function         #<Function 1 subfunction of MAKE-ADDER 4060001CAC>
Environment      #(77 20)

Si noti che la DESCRIBEfunzione mostra che gli oggetti funzione per entrambe le chiusure sono gli stessi, ma l' ambiente è diverso.

Common Lisp rende sia le chiusure che gli oggetti funzione puri (quelli senza ambiente) entrambi funzioni e si possono chiamare entrambi allo stesso modo, qui usando FUNCALL.


1

La principale differenza deriva dalla mancanza di scoping lessicale in C.

Un puntatore a funzione è proprio questo, un puntatore a un blocco di codice. Qualsiasi variabile non di stack a cui fa riferimento è globale, statica o simile.

Una chiusura, OTOH, ha il proprio stato sotto forma di "variabili esterne" o "valori superiori". possono essere privati ​​o condivisi come si desidera, utilizzando lo scoping lessicale. È possibile creare molte chiusure con lo stesso codice funzione, ma istanze di variabili diverse.

Alcune chiusure possono condividere alcune variabili, e quindi può essere l'interfaccia di un oggetto (nel senso OOP). per farlo in C devi associare una struttura a una tabella di puntatori a funzione (questo è quello che fa C ++, con una classe vtable).

in breve, una chiusura è un puntatore a funzione PIÙ uno stato. è un costrutto di livello superiore


2
WTF? C ha sicuramente un ambito lessicale.
Luís Oliveira

1
ha "scoping statico". a quanto mi risulta, lo scoping lessicale è una caratteristica più complessa per mantenere una semantica simile su un linguaggio che ha funzioni create dinamicamente, che vengono poi chiamate chiusure.
Javier,

1

La maggior parte delle risposte indica che le chiusure richiedono puntatori a funzione, possibilmente a funzioni anonime, ma come ha scritto Mark le chiusure possono esistere con funzioni con nome. Ecco un esempio in Perl:

{
    my $count;
    sub increment { return $count++ }
}

La chiusura è l'ambiente che definisce la $countvariabile. È disponibile solo per la incrementsubroutine e persiste tra le chiamate.


0

In C un puntatore a funzione è un puntatore che richiama una funzione quando viene dereferenziato, una chiusura è un valore che contiene la logica di una funzione e l'ambiente (le variabili ei valori a cui sono associati) e un lambda di solito si riferisce a un valore che è in realtà una funzione senza nome. In C una funzione nonèun valore di prima classe quindi non puòessere passata quindi devi passare un puntatore ad essa invece, tuttavia nei linguaggi funzionali (come Scheme) puoi passare le funzioni nello stesso modo in cui passi qualsiasi altro valore

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.