Qual è la differenza tra una continuazione e una richiamata?

Ho navigato in tutto il Web alla ricerca di chiarimenti sulle continuazioni, ed è incredibile quanto la più semplice delle spiegazioni possa confondere così profondamente un programmatore JavaScript come me. Ciò è particolarmente vero quando la maggior parte degli articoli spiega continuazioni con codice in Schema o usa monadi.

Ora che finalmente penso di aver capito l’essenza delle continuazioni, volevo sapere se quello che so è effettivamente la verità. Se ciò che penso sia vero non è in realtà vero, quindi è ignoranza e non illuminazione.

Quindi, ecco quello che so:

In quasi tutte le lingue, le funzioni restituiscono esplicitamente i valori (e il controllo) al chiamante. Per esempio:

var sum = add(2, 3); console.log(sum); function add(x, y) { return x + y; } 

    Ora in una lingua con funzioni di prima class possiamo passare il controllo e restituire il valore a un callback invece di tornare esplicitamente al chiamante:

     add(2, 3, function (sum) { console.log(sum); }); function add(x, y, cont) { cont(x + y); } 

    Quindi, invece di restituire un valore da una funzione, stiamo continuando con un’altra funzione. Quindi questa funzione è chiamata una continuazione del primo.

    Quindi qual è la differenza tra una continuazione e una richiamata?

    Credo che le continuazioni siano un caso speciale di callback. Una funzione può richiamare un numero qualsiasi di funzioni, un numero qualsiasi di volte. Per esempio:

     var array = [1, 2, 3]; forEach(array, function (element, array, index) { array[index] = 2 * element; }); console.log(array); function forEach(array, callback) { var length = array.length; for (var i = 0; i < length; i++) callback(array[i], array, i); } 

    Nonostante la meravigliosa recensione, penso che confonderai la tua terminologia un po ‘. Ad esempio, sei corretto che una chiamata coda avvenga quando la chiamata è l’ultima cosa che una funzione deve eseguire, ma in relazione alle continuazioni, una chiamata coda significa che la funzione non modifica la continuazione con la quale è chiamata, solo che aggiorna il valore passato alla continuazione (se lo desidera). Questo è il motivo per cui la conversione di una funzione ricorsiva di coda in CPS è così semplice (basta aggiungere la continuazione come parametro e chiamare la continuazione sul risultato).

    È anche un po ‘strano chiamare continuations un caso particolare di callback. Riesco a vedere come sono facilmente raggruppati insieme, ma le continuazioni non nascono dalla necessità di distinguere da un callback. Una continuazione rappresenta in realtà le istruzioni che rimangono per completare un calcolo , o il resto del calcolo da questo punto nel tempo. Puoi pensare a una continuazione come a un buco che deve essere riempito. Se riesco a catturare la continuazione corrente di un programma, allora posso tornare esattamente a come era il programma quando ho catturato la continuazione. (Ciò rende i debugger più facili da scrivere).

    In questo contesto, la risposta alla tua domanda è che una richiamata è una cosa generica che viene richiamata in qualsiasi momento specificato da un contratto fornito dal chiamante [della richiamata]. Un callback può avere tutte le argomentazioni che desidera e essere strutturato nel modo desiderato. Una continuazione , quindi, è necessariamente una procedura a un argomento che risolve il valore passato in essa. Una continuazione deve essere applicata a un singolo valore e l’applicazione deve avvenire alla fine. Quando una continuazione termina l’esecuzione dell’espressione è completa, e, a seconda della semantica della lingua, gli effetti collaterali possono o non possono essere stati generati.

    La risposta breve è che la differenza tra una continuazione e una callback è che dopo che un callback è stato richiamato (e terminato) l’esecuzione riprende nel punto in cui è stata richiamata, mentre invocando una continuazione causa la ripresa dell’esecuzione nel punto in cui è stata creata la continuazione. In altre parole: una continuazione non ritorna mai .

    Considera la funzione:

     function add(x, y, c) { alert("before"); c(x+y); alert("after"); } 

    (Io uso la syntax di Javascript anche se Javascript non supporta effettivamente continuazioni di prima class perché questo è ciò che hai dato ai tuoi esempi, e sarà più comprensibile per le persone che non hanno familiarità con la syntax Lisp).

    Ora, se gli passiamo una richiamata:

     add(2, 3, function (sum) { alert(sum); }); 

    quindi vedremo tre avvisi: “prima”, “5” e “dopo”.

    D’altra parte, se dovessimo passare una continuazione che fa la stessa cosa che fa il callback, in questo modo:

     alert(callcc(function(cc) { add(2, 3, cc); })); 

    quindi vedremmo solo due avvisi: “prima” e “5”. Il richiamo di c() all’interno di add() termina l’esecuzione di add() e causa il callcc() ; il valore restituito da callcc() era il valore passato come argomento a c (cioè la sum).

    In questo senso, anche se invocare una continuazione sembra una chiamata di funzione, è in qualche modo più simile a un’istruzione return o all’eccezione.

    In effetti, call / cc può essere utilizzato per aggiungere dichiarazioni di ritorno a lingue che non le supportano. Ad esempio, se JavaScript non ha un’istruzione return (invece, come molti linguaggi Lips, restituisce solo il valore dell’ultima espressione nel corpo della funzione) ma ha call / cc, potremmo implementare return in questo modo:

     function find(myArray, target) { callcc(function(return) { var i; for (i = 0; i < myArray.length; i += 1) { if(myArray[i] === target) { return(i); } } return(undefined); // Not found. }); } 

    Calling return(i) richiama una continuazione che termina l'esecuzione della funzione anonima e fa sì che callcc() restituisca l'indice i al quale è stato trovato il target in myArray .

    (NB: ci sono alcuni modi in cui l'analogia del "ritorno" è un po 'semplicistica. Ad esempio, se una continuazione sfugge dalla funzione in cui è stata creata, salvata in un globale da qualche parte, diciamo ... è ansible che la funzione che ha creato la continuazione può tornare più volte anche se è stato invocato solo una volta .)

    Call / cc può essere utilizzato allo stesso modo per implementare la gestione delle eccezioni (throw e try / catch), i loop e molte altre strutture di controllo.

    Per chiarire alcuni possibili malintesi:

    • L'ottimizzazione della chiamata di coda non è affatto necessaria per supportare continuazioni di prima class. Considera che anche il linguaggio C ha una forma (ristretta) di continuazioni sotto forma di setjmp() , che crea una continuazione, e longjmp() , che invoca uno!

      • D'altra parte, se si tenta ingenuamente di scrivere il proprio programma in uno stile di passaggio di continuazione senza ottimizzazione delle chiamate tail, si rischia di sovraccaricare lo stack.
    • Non c'è una ragione particolare per cui una continuazione richieda solo un argomento. È solo che l'argomento (i) della continuazione diventa il valore restituito (s) di call / cc, e call / cc viene in genere definito come con un singolo valore di ritorno, quindi naturalmente la continuazione deve averne esattamente uno. Nelle lingue con supporto per più valori di ritorno (come Common Lisp, Go o effettivamente Scheme) sarebbe del tutto ansible avere continuazioni che accettano più valori.