haskell - Lazy Evaluation vs Macros
lisp scheme (5)
Estoy acostumbrado a la evaluación perezosa de Haskell, y me encuentro irritado con los idiomas ansiosos por defecto ahora que he usado la evaluación perezosa correctamente. Esto es realmente bastante dañino, ya que los otros lenguajes que uso principalmente hacen que la evaluación perezosa de las cosas sea muy incómoda, lo que normalmente implica el despliegue de iteradores personalizados, entre otros. Entonces, al adquirir algunos conocimientos, me he vuelto menos productivo en mis idiomas originales. Suspiro.
Pero escuché que las macros AST ofrecen otra forma limpia de hacer lo mismo. A menudo he escuchado declaraciones como ''La evaluación diferida hace que las macros sean redundantes'' y viceversa, en su mayoría de las comunidades Lisp y Haskell.
He incursionado con macros en diversas variantes de Lisp. Simplemente parecían una forma realmente organizada de copiar y pegar trozos de código que se manejaban en tiempo de compilación. Ciertamente no eran el santo grial que a Lispers le gusta pensar que es. Pero eso es casi seguro porque no puedo usarlos correctamente. Por supuesto, hacer que el macro sistema funcione en la misma estructura de datos central con la que se ensambla el lenguaje es realmente útil, pero sigue siendo básicamente una forma organizada de copiar y pegar el código. Reconozco que basar un macro sistema en el mismo AST que el lenguaje que permite la alteración completa del tiempo de ejecución es poderoso.
Lo que quiero saber es, ¿cómo se pueden usar las macros para hacer concisa y sucintamente lo que hace la evaluación perezosa? Si quiero procesar un archivo línea por línea sin tomar todo el asunto, simplemente devuelvo una lista que tiene una rutina de lectura de líneas mapeada. Es el ejemplo perfecto de DWIM (haz lo que quiero decir). Ni siquiera tengo que pensar en eso.
Claramente no tengo macros. Los he usado y no me ha impresionado particularmente dado el bombo. Entonces me falta algo que no estoy leyendo la documentación en línea. ¿Puede alguien explicarme todo esto?
La evaluación diferida hace que las macros sean redundantes
Esto es pura tontería (no es tu culpa, lo he escuchado antes). Es cierto que puede usar macros para cambiar el orden, el contexto, etc. de la evaluación de expresiones, pero ese es el uso más básico de las macros, y no es realmente práctico simular un lenguaje lento utilizando macros ad-hoc en lugar de funciones. Entonces, si vinieras en macros desde esa dirección, de hecho estarías decepcionado.
Las macros son para extender el lenguaje con nuevas formas sintácticas. Algunas de las capacidades específicas de las macros son
- Afectar el orden, el contexto, etc. de la evaluación de la expresión.
- Creando nuevas formas de enlace (es decir, afectando el alcance en que se evalúa una expresión).
- Realización de cómputos en tiempo de compilación, incluido el análisis y la transformación del código.
Las macros que sí lo hacen (1) pueden ser bastante simples. Por ejemplo, en Racket , el formulario de manejo de excepciones, with-handlers
, es solo una macro que se expande en call-with-exception-handler
, algunos condicionales y algunos códigos de continuación. Se usa así:
(with-handlers ([(lambda (e) (exn:fail:network? e))
(lambda (e)
(printf "network seems to be broken/n")
(cleanup))])
(do-some-network-stuff))
La macro implementa la noción de "cláusulas predicado-y-manejador en el contexto dinámico de la excepción" basada en el call-with-exception-handler
primitivo de call-with-exception-handler
que maneja todas las excepciones en el punto donde se generan.
Un uso más sofisticado de macros es una implementación de un generador de analizador LALR (1) . En lugar de un archivo separado que necesita preprocesamiento, la forma del parser
es solo otro tipo de expresión. Toma una descripción gramatical, calcula las tablas en tiempo de compilación y produce una función de analizador. Las rutinas de acción tienen un alcance léxico, por lo que pueden hacer referencia a otras definiciones en el archivo o incluso a las variables lambda
-bound. Incluso puede usar otras extensiones de lenguaje en las rutinas de acción.
En el extremo, Typed Racket es un dialecto tipeado de Racket implementado a través de macros. Tiene un sistema de tipo sofisticado diseñado para adaptarse a las expresiones idiomáticas del código Racket / Scheme, e interopera con módulos sin tipo protegiendo las funciones tipeadas con contratos dinámicos de software (también implementados a través de macros). Se implementa mediante una macro "módulo tipado" que expande, revisa y transforma el cuerpo del módulo, así como las macros auxiliares para adjuntar información de tipo a las definiciones, etc.
FWIW, también hay Lazy Racket , un dialecto perezoso de Racket. No se implementa convirtiendo cada función en una macro, sino volviendo a vincular lambda
, define
y la sintaxis de la aplicación de función a macros que crean y fuerzan promesas.
En resumen, la evaluación diferida y las macros tienen un pequeño punto de intersección, pero son cosas extremadamente diferentes. Y las macros ciertamente no son subsumidas por la evaluación perezosa.
La evaluación diferida puede sustituir ciertos usos de macros (los que retrasan la evaluación para crear construcciones de control) pero lo contrario no es cierto. Puede usar macros para hacer las construcciones de evaluación retrasadas más transparentes. Consulte SRFI 41 (Flujos) para ver un ejemplo de cómo: http://download.plt-scheme.org/doc/4.1.5/html/srfi-std/srfi-41/srfi-41.html
Además de esto, también puedes escribir tus propias primitivas IO perezosas.
En mi experiencia, sin embargo, el código perezosamente extendido en un lenguaje estricto tiende a introducir una sobrecarga en comparación con el código omnipresente en un tiempo de ejecución diseñado para soportarlo de manera eficiente desde el principio, lo que, en su opinión, es un problema de implementación realmente.
La pereza es denotative , mientras que las macros no lo son. Más precisamente, si agrega un carácter no estricto a un lenguaje denotativo, el resultado sigue siendo denotativo, pero si agrega macros, el resultado no es denotativo. En otras palabras, el significado de una expresión en un lenguaje perezoso puro es una función únicamente de los significados de las expresiones componentes; mientras que las macros pueden arrojar resultados semánticamente distintos de argumentos semánticamente iguales.
En este sentido, las macros son más poderosas, mientras que la pereza se comporta mejor semánticamente.
Editar : más precisamente, las macros son no denotativas, excepto con respecto a la denotación identidad / trivial (donde la noción de "denotativo" se vuelve vacía).
Las macros se pueden usar para manejar la evaluación diferida, pero solo son parte de esto. El punto principal de las macros es que gracias a ellas, básicamente, nada está solucionado en el lenguaje.
Si la programación es como jugar con ladrillos LEGO, con las macros también puede cambiar la forma de los ladrillos o el material con el que están construidos.
Macros es más que una evaluación tardía. Eso estaba disponible como fexpr
(un macro precursor en la historia de lisp). Macros se trata de la reescritura de programas, donde fexpr
es solo un caso especial ...
Como ejemplo, considero que escribo en mi tiempo libre un pequeño ceceo al compilador de JavaScript y originalmente (en el kernel de javascript) solo tenía lambda con soporte para argumentos &rest
. Ahora hay soporte para argumentos de palabras clave y eso porque redefiní lo que significa lambda en el propio lisp.
Ahora puedo escribir:
(defun foo (x y &key (z 12) w) ...)
y llamar a la función con
(foo 12 34 :w 56)
Al ejecutar esa llamada, en el cuerpo de la función el parámetro w
se vinculará a 56 y el parámetro z
a 12 porque no se pasó. También recibiré un error de tiempo de ejecución si se pasa un argumento de palabra clave no compatible a la función. Incluso podría agregar algún soporte de verificación en tiempo de compilación redefiniendo lo que significa compilar una expresión (es decir, agregar comprobaciones si los formularios de llamada de función "estáticos" están pasando los parámetros correctos a las funciones).
El punto central es que el lenguaje original (kernel) no tenía soporte para los argumentos de palabra clave, y pude agregarlo usando el lenguaje mismo. El resultado es exactamente como si estuviera allí desde el principio; es simplemente parte del lenguaje.
La sintaxis es importante (incluso si es técnicamente posible usar una máquina de turing). La sintaxis da forma a los pensamientos que tienes. Las macros (y las macros de lectura) le dan control total sobre la sintaxis.
Un punto clave es que el código de reescritura de código no utiliza un lenguaje simplificado como el de un idiota ** k-like como la metaprogramación de plantillas de C ++ (donde hacer solo un if
es un logro increíble), o con un incluso más tonto que un regexp motor de sustitución como el preprocesador C
El código de reescritura de código usa el mismo lenguaje completo (y extensible). Está ceceo todo el camino ;-)
Claro que escribir macros es más difícil que escribir código regular; pero es una "complejidad esencial" del problema, no una complejidad artificial porque estás obligado a usar un medio lenguaje tonto como con la metaprogramación de C ++.
Escribir macros es más difícil porque el código es algo complejo y cuando se escriben macros se escriben cosas complejas que construyen cosas complejas por sí mismas. Incluso no es tan raro subir un nivel más y escribir macros de generación de macros (ahí es donde viene la vieja broma de lisp de "Estoy escribiendo código que escribe código que escribe el código que me está pagando").
Pero el poder macro no tiene límites.
Lisp comenzó a finales de los años 50 del último milenio. Ver FUNCIONES RECURSIVAS DE EXPRESIONES SIMBÓLICAS Y SU COMPUTACIÓN POR MÁQUINA . Las macros no eran parte de ese Lisp. La idea era computar con expresiones simbólicas, que pueden representar todo tipo de fórmulas y programas: expresiones matemáticas, expresiones lógicas, oraciones en lenguaje natural, programas de computadora, ...
Posteriormente se inventaron las macros de Lisp y son una aplicación de esa idea anterior a Lisp: las macros transforman las expresiones Lisp (o Lisp-like) en otras expresiones Lisp usando el lenguaje Lisp completo como un lenguaje de transformación.
Puedes imaginar que con Macros puedes implementar poderosos preprocesadores y compiladores como usuario de Lisp.
El dialecto típico de Lisp utiliza una evaluación estricta de los argumentos: todos los argumentos a las funciones se evalúan antes de que se ejecute una función. Lisp también tiene varios formularios integrados que tienen diferentes reglas de evaluación. IF
es un ejemplo. En Common Lisp IF
es un llamado operador especial .
Pero podemos definir un nuevo lenguaje (sub) tipo Lisp que usa evaluación diferida y podemos escribir Macros para transformar ese lenguaje en Lisp. Esta es una aplicación para macros, pero de lejos no es la única.
Un ejemplo (relativamente antiguo) para dicha extensión Lisp que utiliza macros para implementar un transformador de código que proporciona estructuras de datos con evaluación diferida es la extensión SERIES para Common Lisp.