lisp scheme common-lisp continuations callcc

¿Por qué no existe un primitivo `call-with-current-continuations` en Common Lisp?



scheme common-lisp (4)

Últimamente he estado investigando las diferencias entre Scheme y Common Lisp con respecto al enfoque que estos dos idiomas tienen para las continuaciones.

Me he dado cuenta de que el enfoque de Common Lisp es más conservador que el que tiene Scheme.

Además, Scheme ofrece una call-with-current-continuation primitiva call-with-current-continuation , call/cc comúnmente abreviada, que no tiene equivalente en la especificación ANSI Common Lisp (aunque hay algunas bibliotecas que intentan implementarlas).

¿Alguien sabe la razón por la que se tomó la decisión de no crear una primitiva similar en la especificación ANSI Common Lisp?

Gracias por adelantado.


Common Lisp es el resultado de un esfuerzo de estandarización en varios tipos de Lisps prácticos (aplicados) (por lo tanto, "Common"). CL está orientado a aplicaciones de la vida real, por lo tanto, tiene características más "específicas" (como handler-bind ) en lugar de call/cc .

Scheme fue diseñado como un lenguaje pequeño y limpio para la enseñanza de la CS, por lo que tiene el call/cc fundamental que se puede usar para implementar otras herramientas.

Consulte también ¿Se puede implementar la llamada con la continuación de la corriente solo con lambdas y cierres?


Common Lisp tiene un modelo detallado de compilación de archivos como parte del lenguaje estándar. El modelo permite compilar el programa para objetar archivos en un entorno y cargarlos en una imagen en otro entorno. No hay nada comparable en el esquema. No se eval-when , ni compile-file , el load-time-value o conceptos como qué es un objeto externalizable, cómo la semántica en el código compilado debe concordar con el código interpretado. Lisp tiene una forma de tener funciones en línea o no tenerlas en línea, y básicamente, usted controla con gran precisión lo que sucede cuando se recarga un módulo compilado.

Por el contrario, hasta una revisión reciente del informe del Esquema, el lenguaje del Esquema fue completamente silencioso sobre el tema de cómo un programa del Esquema se divide en varios archivos. No se proporcionaron funciones o macros para esto. Mira R5RS, bajo 6.6.4 Interfaz del sistema . Todo lo que tienes allí es una función de load muy vagamente definida:

procedimiento opcional: (cargar nombre de archivo)

El nombre del archivo debe ser una cadena que nombre un archivo existente que contenga el código fuente de Scheme. El procedimiento de carga lee expresiones y definiciones del archivo y las evalúa de forma secuencial. No se especifica si se imprimen los resultados de las expresiones. El procedimiento de carga no afecta los valores devueltos por current-input-port y current-output-port. La carga devuelve un valor no especificado.

Justificación: para la portabilidad, la carga debe operar en archivos de origen. Su funcionamiento en otros tipos de archivos varía necesariamente entre las implementaciones.

Entonces, si ese es el alcance de su visión acerca de cómo se construyen las aplicaciones a partir de módulos, y todos los detalles más allá de eso se dejan a los implementadores para que trabajen, por supuesto, el cielo es el límite para inventar la semántica del lenguaje de programación. Tenga en cuenta, en parte, la parte Racional: si la load se define como operar en archivos de origen (y todo lo demás es una cortesía adicional de los implementadores), entonces no es más que un mecanismo de inclusión textual como #include en el lenguaje C, y por lo tanto el Esquema la aplicación es en realidad solo un cuerpo de texto que se distribuye físicamente en varios archivos de texto reunidos por load .

Si está pensando en agregar cualquier característica a Common Lisp, debe pensar cómo encaja en su detallado modelo dinámico de carga y compilación, mientras conserva el buen rendimiento que esperan los usuarios.

Si la función en la que está pensando requiere una optimización global de todo el programa (por lo que el sistema necesita ver el código fuente estructural de todo) para que los programas de los usuarios no se ejecuten de manera deficiente (y en particular los programas que no usan esa función) ) entonces realmente no volará.

Específicamente con respecto a la semántica de las continuaciones, hay problemas. En la semántica habitual de un ámbito de bloque, una vez que dejamos un ámbito y realizamos la limpieza, eso se ha ido; No podemos volver a ese alcance a tiempo y reanudar el cálculo. Common Lisp es ordinario de esa manera. Tenemos la construcción de unwind-protect que realiza acciones de limpieza incondicionales cuando finaliza un ámbito. Esta es la base para características como with-open-file que proporciona un objeto de identificador de archivo abierto a un ámbito de bloque y garantiza que se cierre sin importar cómo termine el ámbito de bloque. Si una continuación se escapa de ese ámbito, esa continuación ya no tiene un archivo válido. No podemos simplemente no cerrar el archivo cuando dejamos el alcance porque no hay seguridad de que alguna vez se usará la continuación; es decir, debemos asumir que el alcance se está abandonando para siempre y limpiar el recurso de manera oportuna. La solución de curita para este tipo de problema es dynamic-wind , que nos permite agregar controladores en la entrada y salida a un ámbito de bloque. Por lo tanto, podemos volver a abrir el archivo cuando el bloque se reinicia por una continuación. Y no solo vuelva a abrirlo, sino que también coloque la secuencia exactamente en la misma posición en el archivo y así sucesivamente. Si la secuencia estaba a mitad de camino a través de la decodificación de algún carácter UTF-8, debemos ponerlo en el mismo estado. Por lo tanto, si Lisp tuviera continuaciones, se romperían con varias construcciones que realizan una limpieza (integración deficiente) o bien esas construcciones tendrían que adquirir una semántica mucho más peluda.

Hay alternativas a las continuaciones. Algunos usos de las continuaciones no son esenciales. Esencialmente, la misma organización de código se puede obtener con cierres o reinicios. Además, hay una construcción de sistema operativo / lenguaje potente que puede competir con la continuación: a saber, el hilo. Si bien las continuaciones tienen aspectos que no están bien modelados por subprocesos (y sin mencionar que no introducen puntos muertos y condiciones de carrera en el código), también tienen desventajas en comparación con los subprocesos: como la falta de concurrencia real para la utilización de múltiples procesadores, o priorización Muchos problemas expresables con continuaciones pueden expresarse con hilos casi con la misma facilidad. Por ejemplo, las continuaciones nos permiten escribir un analizador de descenso recursivo que se parece a un objeto parecido a un flujo que simplemente devuelve resultados progresivos a medida que analiza. El código es en realidad un analizador de descenso recursivo y no una máquina de estados que simula uno. Los subprocesos nos permiten hacer lo mismo: podemos colocar el analizador en un subproceso envuelto en un "objeto activo", que tiene algún método de "obtener lo siguiente" que extrae cosas de una cola. A medida que el hilo analiza, en lugar de devolver una continuación, simplemente lanza objetos en una cola (y posiblemente bloquea para que otro hilo los elimine). La continuación de la ejecución se proporciona al reanudar ese hilo; Su contexto de hilo es la continuación. No todos los modelos de roscas sufren condiciones de carrera (tanto); hay, por ejemplo, subprocesos cooperativos, bajo los cuales se ejecuta un subproceso a la vez, y los cambios de subprocesos solo tienen lugar cuando un subproceso realiza una llamada explícita al núcleo de subprocesos. Las principales implementaciones de Common Lisp han tenido subprocesos ligeros (generalmente llamados "procesos") durante décadas, y gradualmente se han movido hacia subprocesos más sofisticados con soporte de multiprocesamiento. El soporte para subprocesos reduce la necesidad de continuar, y es una mayor prioridad de implementación porque los tiempos de ejecución del lenguaje sin el soporte de subprocesos están en desventaja tecnológica: la incapacidad de aprovechar al máximo los recursos de hardware.


El diseño del esquema se basó en el uso de llamadas a funciones para reemplazar las estructuras de control más comunes. Esta es la razón por la que Scheme requiere la eliminación de la llamada de cola: permite que un bucle se convierta en una llamada recursiva sin potencialmente quedarse sin espacio en la pila. Y el enfoque subyacente de esto es el estilo de paso de continuación .

Common Lisp es más práctico y menos pedagógico. No dicta estrategias de implementación, y no se requieren continuaciones para implementarla.


Esto es lo que Kent M. Pitman, uno de los diseñadores de Common Lisp, dijo sobre el tema: de comp.lang.lisp