He estado navegando por toda la web en busca de iluminación sobre las continuaciones, y es alucinante cómo las explicaciones más simples pueden confundir por completo a un programador de JavaScript como yo. Esto es especialmente cierto cuando la mayoría de los artículos explican las continuaciones con código en Scheme o usan mónadas.
Ahora que finalmente creo haber entendido la esencia de las continuaciones, quería saber si lo que sé es realmente la verdad. Si lo que creo que es verdad no es verdad, entonces es ignorancia y no iluminación.
Entonces, esto es lo que sé:
En casi todos los idiomas, las funciones devuelven explícitamente valores (y control) a la persona que llama. Por ejemplo:
var sum = add(2, 3);
console.log(sum);
function add(x, y) {
return x + y;
}
Ahora, en un lenguaje con funciones de primera clase, podemos pasar el control y el valor de retorno a una devolución de llamada en lugar de regresar explícitamente a la persona que llama:
add(2, 3, function (sum) {
console.log(sum);
});
function add(x, y, cont) {
cont(x + y);
}
Por lo tanto, en lugar de devolver un valor de una función, continuamos con otra función. Por lo tanto, esta función se llama continuación de la primera.
Entonces, ¿cuál es la diferencia entre una continuación y una devolución de llamada?
fuente
Respuestas:
Creo que las continuaciones son un caso especial de devoluciones de llamada. Una función puede devolver cualquier cantidad de funciones, cualquier cantidad de veces. Por ejemplo:
Sin embargo, si una función vuelve a llamar a otra función como lo último que hace, la segunda función se llama continuación de la primera. Por ejemplo:
Si una función llama a otra función como lo último que hace, entonces se llama una llamada de cola. Algunos lenguajes como Scheme realizan optimizaciones de llamada de cola. Esto significa que la llamada de cola no incurre en la sobrecarga total de una llamada de función. En cambio, se implementa como un simple goto (con el marco de la pila de la función de llamada reemplazado por el marco de la pila de la llamada de cola).
Bonificación : proceder al estilo de pase de continuación. Considere el siguiente programa:
Ahora, si cada operación (incluida la suma, multiplicación, etc.) se escribiera en forma de funciones, tendríamos:
Además, si no se nos permitiera devolver ningún valor, tendríamos que usar las siguientes continuaciones:
Este estilo de programación en el que no se le permite devolver valores (y, por lo tanto, debe recurrir a pasar las continuaciones) se llama estilo de paso de continuación.
Sin embargo, hay dos problemas con el estilo de pase de continuación:
El primer problema se puede resolver fácilmente en JavaScript llamando a las continuaciones de forma asincrónica. Al llamar a la continuación de forma asincrónica, la función vuelve antes de que se llame a la continuación. Por lo tanto, el tamaño de la pila de llamadas no aumenta:
El segundo problema generalmente se resuelve usando una función llamada
call-with-current-continuation
que a menudo se abrevia comocallcc
. Desafortunadamentecallcc
, no se puede implementar completamente en JavaScript, pero podríamos escribir una función de reemplazo para la mayoría de sus casos de uso:La
callcc
función toma una funciónf
y la aplica acurrent-continuation
(abreviada comocc
). Lacurrent-continuation
es una función de continuación que envuelve el resto del cuerpo de la función después de la llamada acallcc
.Considere el cuerpo de la función
pythagoras
:El
current-continuation
de la segundacallcc
es:Del mismo modo, el
current-continuation
primerocallcc
es:Como el
current-continuation
primerocallcc
contiene otrocallcc
, debe convertirse al estilo de paso de continuación:Así que, esencialmente,
callcc
convierte lógicamente todo el cuerpo de la función a lo que comenzamos (y le da el nombre a esas funciones anónimascc
). La función de Pitágoras que usa esta implementación de callcc se convierte en:Nuevamente, no puede implementar
callcc
en JavaScript, pero puede implementar el estilo de paso de continuación en JavaScript de la siguiente manera:La función
callcc
se puede utilizar para implementar estructuras de flujo de control complejas, como bloques try-catch, corutinas, generadores, fibras , etc.fuente
A pesar de la maravillosa crítica, creo que estás confundiendo un poco tu terminología. Por ejemplo, tiene razón en que una llamada de cola ocurre cuando la llamada es lo último que necesita ejecutar una función, pero en relación con las continuaciones, una llamada de cola significa que la función no modifica la continuación con la que se llama, solo que actualiza el valor pasado a la continuación (si lo desea). Es por eso que convertir una función recursiva de cola a CPS es tan fácil (simplemente agrega la continuación como parámetro y llama a la continuación en el resultado).
También es un poco extraño llamar a las continuaciones un caso especial de devoluciones de llamada. Puedo ver cómo se agrupan fácilmente, pero las continuidades no surgieron de la necesidad de distinguir de una devolución de llamada. Una continuación en realidad representa las instrucciones restantes para completar un cálculo , o el resto del cálculo desde este punto en el tiempo. Puede pensar en una continuación como un agujero que necesita ser llenado. Si puedo capturar la continuación actual de un programa, entonces puedo volver exactamente a cómo era el programa cuando capturé la continuación. (Eso hace que los depuradores sean más fáciles de escribir).
En este contexto, la respuesta a su pregunta es que una devolución de llamada es una cosa genérica que se llama en cualquier momento especificado por algún contrato proporcionado por la persona que llama [de la devolución de llamada]. Una devolución de llamada puede tener tantos argumentos como desee y estructurarse de la forma que desee. Una continuación , entonces, es necesariamente un procedimiento de un argumento que resuelve el valor que se le pasa. Se debe aplicar una continuación a un valor único y la aplicación debe suceder al final. Cuando una continuación termina de ejecutarse, la expresión se completa y, dependiendo de la semántica del lenguaje, los efectos secundarios pueden o no haberse generado.
fuente
La respuesta corta es que la diferencia entre una continuación y una devolución de llamada es que después de que se invoca una devolución de llamada (y ha finalizado), la ejecución se reanuda en el punto en que se invocó, mientras que la invocación de una continuación hace que la ejecución se reanude en el punto en que se creó la continuación. En otras palabras: una continuación nunca regresa .
Considere la función:
(Uso la sintaxis de Javascript, aunque Javascript no es compatible con las continuaciones de primera clase porque esto fue en lo que usted dio sus ejemplos, y será más comprensible para las personas que no estén familiarizadas con la sintaxis de Lisp).
Ahora, si le pasamos una devolución de llamada:
luego veremos tres alertas: "antes", "5" y "después".
Por otro lado, si tuviéramos que pasarle una continuación que hace lo mismo que la devolución de llamada, así:
entonces veríamos solo dos alertas: "antes" y "5". Invocar
c()
dentroadd()
termina la ejecución deadd()
y hacecallcc()
que regrese; el valor devuelto porcallcc()
fue el valor pasado como argumento ac
(es decir, la suma).En este sentido, aunque invocar una continuación parece una llamada de función, de alguna manera es más parecido a una declaración de retorno o arrojando una excepción.
De hecho, call / cc puede usarse para agregar declaraciones de retorno a idiomas que no las admiten. Por ejemplo, si JavaScript no tenía una declaración de retorno (en cambio, como muchos lenguajes de Lips, solo devolvía el valor de la última expresión en el cuerpo de la función) pero tenía call / cc, podríamos implementar return de esta manera:
Llamar
return(i)
invoca una continuación que finaliza la ejecución de la función anónima y hacecallcc()
que devuelva el índicei
en el quetarget
se encontrómyArray
.(Nota: hay algunas formas en que la analogía de "retorno" es un poco simplista. Por ejemplo, si una continuación se escapa de la función en la que se creó, al guardarla en un lugar global, digamos, es posible que la función que creó la continuación puede regresar varias veces, aunque solo se invocó una vez ).
Call / cc puede usarse de manera similar para implementar el manejo de excepciones (throw and try / catch), bucles y muchas otras estructuras de control.
Para aclarar algunas posibles interpretaciones erróneas:
La optimización de llamadas de cola no se requiere de ninguna manera para admitir continuaciones de primera clase. ¡Tenga en cuenta que incluso el lenguaje C tiene una forma (restringida) de continuaciones en forma de
setjmp()
, que crea una continuación ylongjmp()
que invoca una!No hay ninguna razón particular para que una continuación tome solo un argumento. Es solo que los argumentos para la continuación se convierten en los valores de retorno de call / cc, y call / cc generalmente se define como que tiene un solo valor de retorno, por lo que, naturalmente, la continuación debe tomar exactamente uno. En idiomas con soporte para múltiples valores de retorno (como Common Lisp, Go o Scheme), sería completamente posible tener continuaciones que acepten múltiples valores.
fuente