Problemas al implementar cierres en entornos no funcionales

18

En los lenguajes de programación, los cierres son una característica popular y a menudo deseada. Wikipedia dice (énfasis mío):

En informática, un cierre (...) es una función junto con un entorno de referencia para las variables no locales de esa función. Un cierre permite que una función acceda a variables fuera de su alcance léxico inmediato.

Entonces, un cierre es esencialmente un valor de función (¿anónimo?) Que puede usar variables fuera de su propio alcance. En mi experiencia, esto significa que puede acceder a las variables que están dentro del alcance en su punto de definición.

Sin embargo, en la práctica, el concepto parece ser divergente, al menos fuera de la programación funcional. Diferentes idiomas implementan diferentes semánticas, incluso parece haber guerras de opiniones sobre. Muchos programadores no parecen saber qué son los cierres, viéndolos como poco más que funciones anónimas.

Además, parece que existen obstáculos importantes al implementar cierres. Lo más notable, se suponía que Java 7 los incluiría, pero la característica se retrasó a una versión futura.

¿Por qué los cierres son tan difíciles de entender y realizar? Esta es una pregunta demasiado amplia y vaga, así que permítanme enfocarla más con estas preguntas interconectadas:

  • ¿Hay problemas para expresar cierres en formalismos semánticos comunes (paso pequeño, paso grande, ...)?
  • ¿Los sistemas de tipos existentes no son adecuados para cierres y no se pueden extender fácilmente?
  • ¿Es problemático alinear los cierres con una traducción de procedimiento tradicional basada en pila?

Tenga en cuenta que la pregunta se refiere principalmente a lenguajes de procedimiento, orientados a objetos y scripts en general. Hasta donde yo sé, los lenguajes funcionales no tienen ningún problema.

Rafael
fuente
Buena pregunta. Se han implementado cierres en Scala, y Martin Odersky escribió el compilador Java 1.5, por lo que no está claro por qué no están en Java 7. C # los tiene. (Trataré de escribir una respuesta mejor más tarde)
Dave Clarke
44
Los lenguajes funcionales impuros como Lisp y ML se adaptan perfectamente a los cierres, por lo que no puede haber una razón semántica intrínseca para que sean problemáticos.
Gilles 'SO- deja de ser malvado'
Incluí el artículo porque he estado luchando por imaginar cómo sería una semántica de pequeño paso para los cierres. Es muy posible que los cierres en sí mismos no sean un problema, pero es difícil incluirlos en un lenguaje que no esté diseñado teniendo en cuenta.
Raphael
1
Eche un vistazo a pdfs.semanticscholar.org/73a2/… - Los autores de Lua lo hicieron de una manera muy inteligente y también discutieron los problemas generales de implementar cierres
Bulat

Respuestas:

10

¿Puedo dirigirlo a la página de Wikipedia de problemas de Funarg ? Al menos así es como la gente del compilador solía hacer referencia al problema de implementación del cierre.

Entonces, un cierre es esencialmente un valor de función (¿anónimo?) Que puede usar variables fuera de su propio alcance. En mi experiencia, esto significa que puede acceder a las variables que están dentro del alcance en su punto de definición.

Si bien esta definición tiene sentido, no ayuda a describir el problema de implementar funciones de primera clase en un lenguaje tradicional basado en la pila de tiempo de ejecución. Cuando se trata de problemas de implementación, las funciones de primera clase se pueden dividir aproximadamente en dos clases:

  • Las variables locales en las funciones nunca se usan después de que la función regresa.
  • Las variables locales se pueden usar después de que la función regrese.

El primer caso (funargs hacia abajo) no es tan difícil de implementar y se puede encontrar incluso en los lenguajes de procedimiento más antiguos, como Algol, C y Pascal. C evita el problema, ya que no permite funciones anidadas, pero Algol y Pascal hacen la contabilidad necesaria para permitir que las funciones internas hagan referencia a las variables de la pila de la función externa.

El segundo caso (funargs hacia arriba), por otro lado, requiere que los registros de activación se guarden fuera de la pila, en el montón. Esto significa que es muy fácil perder recursos de memoria a menos que el tiempo de ejecución del idioma incluya un recolector de basura. Si bien casi todo es basura recolectada hoy, requerir una sigue siendo una decisión de diseño importante y lo fue aún más hace algún tiempo.


En cuanto al ejemplo particular de Java, si recuerdo correctamente, el problema principal no era en realidad poder implementar cierres, sino cómo introducirlos al lenguaje de una manera que no fuera redundante con las características existentes (como clases internas anónimas) y eso no chocó con las características existentes (como las excepciones marcadas, un problema que no es trivial para resolver y que la mayoría de la gente no piensa al principio).

También puedo pensar en otras cosas que hacen que las funciones de primera clase sean menos triviales de implementar, como decidir qué hacer con variables "mágicas" como esta , self o super y cómo interactuar con operadores de flujo de control existentes, como break and return (¿queremos permitir devoluciones no locales o no?). Pero al final, la popularidad reciente de las funciones de primera clase parece indicar que los lenguajes que no las tienen en su mayoría lo hacen por razones históricas o debido a alguna decisión de diseño importante desde el principio.

hugomg
fuente
1
¿Conoces algún idioma que distinga los casos ascendentes y descendentes? En los lenguajes .NET, un método genérico que esperaba recibir una función solo hacia abajo podría recibir una estructura del tipo genérico junto con un delegado que recibiría una estructura como byref (en C #, un " refparámetro"). Si la persona que llama encapsula todas las variables de interés en la estructura, el delegado podría estar completamente estático, evitando cualquier necesidad de una asignación de montón. Los compiladores no ofrecen una buena ayuda de sintaxis para tales construcciones, pero el Framework podría admitirlos.
supercat
2
@supercat: Rust tiene múltiples tipos de cierre que le permiten aplicar en tiempo de compilación si una función interna necesitará usar el montón. Sin embargo, esto no significa que una implementación no pueda intentar evitar las asignaciones de montón sin obligarlo a preocuparse por todos esos tipos adicionales. Un compilador puede tratar de inferir la duración de la función o puede usar comprobaciones de tiempo de ejecución para guardar variables perezosamente en el montón solo cuando sea estrictamente necesario (consulte la sección "alcance léxico" del documento de Evolution of Lua para más detalles)
hugomg
5

Podemos ver cómo se implementan los cierres en C #. La escala de las transformaciones que realiza el compilador de C # deja en claro que su forma de implementar cierres es bastante trabajo. Puede haber formas más fáciles de implementar cierres, pero creo que el equipo del compilador de C # sería consciente de esto.

Considere el siguiente pseudo-C # (corté un poco de material específico de C #):

int x = 1;
function f = function() { x++; };
for (int i = 1; i < 10; i++) {
    f();
}
print x; // Should print 9

El compilador transforma esto en algo como esto:

class FunctionStuff {
   int x;
   void theFunction() {
       x++;
   }
}

FunctionStuff theClosureObject = new FunctionStuff();
theClosureObject.x = 1;
for (int i = 1; i < 10; i++) {
    theClosureObject.theFunction();
}
print theClosureObject.x; // Should print 9

(en realidad, la variable f todavía se creará, donde f es un 'delegado' (= puntero de función), pero este delegado todavía está asociado con el objeto theClosureObject; dejé esta parte por claridad para aquellos que no están familiarizados con C #)

Esta transformación es bastante masiva y complicada: considere los cierres dentro de los cierres y la interacción de los cierres con el resto de las características del lenguaje C #. Me imagino que la función se retrasó para Java, ya que Java 7 ya tiene muchas características nuevas.

Alex ten Brink
fuente
Puedo ver a dónde va esto; tener múltiples cierres y el acceso principal al alcance, la misma variable va a ser desordenada.
Raphael
Para ser honesto, esto se debe más al uso del marco OO existente para implementar cierres que a cualquier problema real con ellos. Otros lenguajes solo asignan las variables en una estructura separada, sin método, y luego permiten que varios cierres lo compartan si lo desean.
hugomg
@Raphael: ¿cómo te sientes acerca de los cierres dentro de los cierres? Espera, déjame agregar eso.
Alex ten Brink
5

Para responder parte de tu pregunta. El formalismo descrito por Morrisett y Harper cubre semántica de pasos grandes y pequeños de lenguajes polimórficos de orden superior que contienen cierres. Hay documentos antes de estos que proporcionan el tipo de semántica que está buscando. Mire, por ejemplo, la máquina SECD . Agregar referencias mutables o locales mutables a esta semántica es sencillo. No veo que haya problemas técnicos al proporcionar tal semántica.

Dave Clarke
fuente
¡Gracias por la referencia! No parece ser una lectura ligera, pero eso es de esperar de un trabajo semántico.
Raphael
1
@Raphael: Probablemente haya otros más simples. Intentaré encontrar algo y volver a contactarlo. En cualquier caso, la Figura 8 tiene la semántica que está buscando.
Dave Clarke
Tal vez pueda dar una visión general aproximada resp. ¿Las ideas centrales en tu respuesta?
Raphael
2
@Raphael. Tal vez podría remitirlo a mis notas de clase que uso para un curso de lenguajes de programación, que le brinda una introducción rápida. Busque los folletos 8 y 9.
Uday Reddy
1
Ese enlace aparece muerto o detrás de la autenticación invisible. ( cs.cmu.edu/afs/cs/user/rwh/public/www/home/papers/gcpoly/tr.pdf ). Tengo 403 prohibido.
Ben Fletcher