¿Qué hace que las macros de Lisp sean tan especiales?
homoiconicity (14)
Dado que las respuestas existentes proporcionan buenos ejemplos concretos que explican lo que logran las macros y cómo, tal vez ayude a recopilar algunas de las ideas sobre por qué la instalación de macros es una ganancia significativa en relación con otros idiomas ; Primero de estas respuestas, luego de uno grande:
... en C, tendría que escribir un preprocesador personalizado [que probablemente califique como un programa C lo suficientemente complicado ] ...
- Vatine
Habla con cualquier persona que domine C ++ y pregúntales cuánto tiempo pasaron aprendiendo toda la plantilla de plantillas que necesitan para realizar la metaprogramación de plantillas [que aún no es tan poderosa].
- Matt Curtis
... en Java, tiene que abrirse camino con el tejido de código de bytes, aunque algunos marcos como AspectJ le permiten hacer esto con un enfoque diferente, es fundamentalmente un truco.
- Miguel Ping
DOLIST es similar a Pereach foreach o Python''s para. Java agregó un tipo similar de construcción de bucle con el "mejorado" para bucle en Java 1.5, como parte de JSR-201. Fíjate en la diferencia que hacen las macros. Un programador Lisp que nota un patrón común en su código puede escribir una macro para darse una abstracción a nivel de fuente de ese patrón. Un programador de Java que se da cuenta del mismo patrón debe convencer a Sun de que vale la pena agregar esta abstracción en particular al lenguaje. Luego, Sun tiene que publicar un JSR y convocar a un "grupo de expertos" de toda la industria para eliminar todo. Ese proceso, según Sun, lleva un promedio de 18 meses. Después de eso, todos los escritores de compiladores deben actualizar sus compiladores para admitir la nueva característica. E incluso una vez que el compilador favorito del programador de Java admita la nueva versión de Java, es probable que "todavía" no puedan usar la nueva característica hasta que se les permita romper la compatibilidad de origen con versiones anteriores de Java. Así que una molestia que los programadores de Common Lisp pueden resolver por sí mismos dentro de cinco minutos plaga a los programadores de Java durante años.
Al leer los ensayos de Paul Graham sobre lenguajes de programación, uno pensaría que las macros de Lisp son el único camino a seguir. Como desarrollador ocupado, trabajando en otras plataformas, no he tenido el privilegio de usar macros de Lisp. Como alguien que quiere entender el rumor, explique qué hace que esta característica sea tan poderosa.
Por favor, también relacione esto con algo que entendería de los mundos de Python, Java, C # o C.
En Python tienes decoradores, básicamente tienes una función que toma otra función como entrada. Puede hacer lo que quiera: llamar a la función, hacer otra cosa, ajustar la llamada a la función en una versión de adquisición de recursos, etc., pero no puede echar un vistazo dentro de esa función. Digamos que queríamos hacerlo más poderoso, digamos que su decorador recibió el código de la función como una lista, entonces no solo podría ejecutar la función tal como está, sino que ahora puede ejecutar partes de ella, reordenar las líneas de la función, etc.
En resumen, las macros son transformaciones de código. Permiten introducir muchas nuevas construcciones de sintaxis. Por ejemplo, considere LINQ en C #. En lisp, hay extensiones de lenguaje similares que se implementan mediante macros (por ejemplo, construcción de bucle incorporada, iteración). Las macros disminuyen significativamente la duplicación de código. Las macros permiten incrustar «pequeños lenguajes» (por ejemplo, donde en c # / java se usaría xml para configurar, en un instante se puede lograr lo mismo con las macros). Las macros pueden ocultar dificultades en el uso de bibliotecas.
Por ejemplo, en lisp puedes escribir.
(iter (for (id name) in-clsql-query "select id, name from users" on-database *users-database*)
(format t "User with ID of ~A has name ~A.~%" id name))
y esto oculta todas las cosas de la base de datos (transacciones, cierre de conexión adecuado, obtención de datos, etc.) mientras que en C # requiere crear SqlConnections, SqlCommands, agregar SqlParameters a SqlCommands, hacer un bucle en SqlDataReaders, cerrarlos adecuadamente.
Encontrará un amplio debate sobre la macro lisp aquí .
Un subconjunto interesante de ese artículo:
En la mayoría de los lenguajes de programación, la sintaxis es compleja. Las macros tienen que desarmar la sintaxis del programa, analizarla y volver a ensamblarla. No tienen acceso al analizador del programa, por lo que tienen que depender de las heurísticas y las mejores suposiciones. A veces su análisis de velocidad de corte es incorrecto, y luego se rompen.
Pero Lisp es diferente. Las macros de Lisp tienen acceso al analizador, y es un analizador realmente simple. Una macro Lisp no recibe una cadena, sino una pieza de código fuente previamente analizada en forma de lista, porque la fuente de un programa Lisp no es una cadena; es una lista Y los programas Lisp son realmente buenos para desarmar listas y volver a armarlas. Lo hacen de forma fiable, todos los días.
Aquí hay un ejemplo extendido. Lisp tiene una macro, llamada "setf", que realiza la asignación. La forma más simple de setf es
(setf x whatever)
que establece el valor del símbolo "x" en el valor de la expresión "lo que sea".
Lisp también tiene listas; puede usar las funciones "car" y "cdr" para obtener el primer elemento de una lista o el resto de la lista, respectivamente.
Ahora, ¿qué sucede si desea reemplazar el primer elemento de una lista con un nuevo valor? Hay una función estándar para hacer eso, e increíblemente, su nombre es incluso peor que "auto". Es la rplaca. Pero no tienes que recordar "rplaca", porque puedes escribir
(setf (car somelist) whatever)
Para configurar el coche de somelist.
Lo que realmente está sucediendo aquí es que "setf" es una macro. En el momento de la compilación, examina sus argumentos y ve que el primero tiene la forma (coche ALGO). Se dice a sí mismo "Oh, el programador está tratando de configurar el auto de algo. La función que se usa para eso es ''rplaca''". Y vuelve a escribir el código en silencio para:
(rplaca somelist whatever)
Las macros de Common Lisp esencialmente extienden los "primitivos sintácticos" de su código.
Por ejemplo, en C, la construcción switch / case solo funciona con tipos integrales y si desea usarlo para flotantes o cadenas, se queda con las declaraciones anidadas y las comparaciones explícitas. Tampoco hay forma de que puedas escribir una macro en C para hacer el trabajo por ti.
Pero, dado que una macro lisp es (esencialmente) un programa lisp que toma fragmentos de código como entrada y devuelve código para reemplazar la "invocación" de la macro, puede extender su repertorio de "primitivos" tan lejos como quiera, generalmente terminando Con un programa más legible.
Para hacer lo mismo en C, tendría que escribir un preprocesador personalizado que ingiera su fuente inicial (no del todo C) y emita algo que un compilador de C pueda entender. No es una forma incorrecta de hacerlo, pero no es necesariamente la más fácil.
Las macros de Lisp le permiten decidir cuándo (si lo hay) alguna parte o expresión será evaluada. Para poner un ejemplo simple, piensa en C''s:
expr1 && expr2 && expr3 ...
Lo que esto dice es: Evalúe expr1
y, si es cierto, evalúe expr2
, etc.
Ahora trata de convertir este &&
en una función ... eso es correcto, no puedes. Llamando algo como:
and(expr1, expr2, expr3)
Evaluará los tres exprs
antes de dar una respuesta, independientemente de si expr1
era falso.
Con las macros lisp puedes codificar algo como:
(defmacro && (expr1 &rest exprs)
`(if ,expr1 ;` Warning: I have not tested
(&& ,@exprs) ; this and might be wrong!
nil))
ahora tiene un &&
, al que puede llamar como una función y no evaluará ninguna forma que le pase a menos que sea verdadera.
Para ver cómo esto es útil, contraste:
(&& (very-cheap-operation)
(very-expensive-operation)
(operation-with-serious-side-effects))
y:
and(very_cheap_operation(),
very_expensive_operation(),
operation_with_serious_side_effects());
Otras cosas que puede hacer con macros es crear nuevas palabras clave y / o mini-idiomas (consulte la macro (loop ...)
para ver un ejemplo), integrando otros idiomas en lisp, por ejemplo, podría escribir una macro que le permita decir algo como:
(setvar *rows* (sql select count(*)
from some-table
where column1 = "Yes"
and column2 like "some%string%")
Y eso ni siquiera se está metiendo en las macros de Reader .
Espero que esto ayude.
Las macros de Lisp representan un patrón que ocurre en casi cualquier proyecto de programación importante. Finalmente, en un programa grande tienes una determinada sección de código en la que te das cuenta de que sería más simple y menos propenso a errores escribir un programa que genere código fuente como texto que luego puedes pegar.
En Python, los objetos tienen dos métodos __repr__
y __str__
. __str__
es simplemente la representación legible por humanos. __repr__
devuelve una representación que es un código de Python válido, es decir, algo que se puede ingresar en el intérprete como Python válido. De esta manera, puedes crear pequeños fragmentos de Python que generan un código válido que se puede pegar en tu fuente real.
En Lisp todo este proceso ha sido formalizado por el sistema macro. Claro que le permite crear extensiones a la sintaxis y hacer todo tipo de cosas elegantes, pero su utilidad real se resume en lo anterior. Por supuesto, es útil que el sistema macro de Lisp le permita manipular estos "fragmentos" con toda la potencia de todo el lenguaje.
Lo obtuve de The common lisp cookbook pero creo que explica por qué las macros lisp son buenas de una manera agradable.
"Una macro es una pieza ordinaria de código Lisp que opera en otra pieza de código Lisp putativo, traduciéndola a (una versión más cercana a) Lisp ejecutable. Eso puede sonar un poco complicado, así que vamos a dar un ejemplo simple. Supongamos que quiere un versión de setq que establece dos variables al mismo valor. Por lo tanto, si escribe
(setq2 x y (+ z 3))
cuando z=8
tanto x como y se establecen en 11. (No se me ocurre ningún uso para esto, pero es solo un ejemplo).
Debería ser obvio que no podemos definir setq2 como una función. Si x=50
y y=-5
, esta función recibiría los valores 50, -5 y 11; no tendría conocimiento de qué variables debían ser establecidas. Lo que realmente queremos decir es: Cuando usted (el sistema Lisp) ve (setq2 v1 v2 e)
, lo trata como equivalente a (progn (setq v1 e) (setq v2 e))
. En realidad, esto no está bien, pero lo hará por ahora. Una macro nos permite hacer precisamente esto, especificando un programa para transformar el patrón de entrada (setq2 v1 v2 e)
"en el patrón de salida (progn ...)
".
Si pensaste que esto era bueno, puedes seguir leyendo aquí: http://cl-cookbook.sourceforge.net/macros.html
Mientras que todo lo anterior explica qué son las macros e incluso tienen ejemplos geniales, creo que la diferencia clave entre una macro y una función normal es que LISP evalúa todos los parámetros antes de llamar a la función. Con una macro es lo contrario, LISP pasa los parámetros sin evaluar a la macro. Por ejemplo, si pasa (+ 1 2) a una función, la función recibirá el valor 3. Si pasa esto a una macro, recibirá una Lista (+ 1 2). Esto se puede utilizar para hacer todo tipo de cosas increíblemente útiles.
- Agregar una nueva estructura de control, por ejemplo, un bucle o la deconstrucción de una lista
Mida el tiempo que se tarda en ejecutar una función pasada. Con una función, el parámetro se evaluaría antes de pasar el control a la función. Con la macro, puede empalmar su código entre el inicio y el final de su cronómetro. El siguiente tiene el mismo código exacto en una macro y una función y la salida es muy diferente. Nota: Este es un ejemplo artificial y la implementación se eligió de modo que sea idéntica para resaltar mejor la diferencia.
(defmacro working-timer (b) (let ( (start (get-universal-time)) (result (eval b))) ;; not splicing here to keep stuff simple ((- (get-universal-time) start)))) (defun my-broken-timer (b) (let ( (start (get-universal-time)) (result (eval b))) ;; doesn''t even need eval ((- (get-universal-time) start)))) (working-timer (sleep 10)) => 10 (broken-timer (sleep 10)) => 0
No creo que haya visto explicadas las macros de Lisp mejor que por este compañero: http://www.defmacro.org/ramblings/lisp.html
No estoy seguro de poder agregar alguna información a las publicaciones (excelentes) de todos, pero ...
Las macros de Lisp funcionan muy bien debido a la naturaleza de sintaxis de Lisp.
Lisp es un lenguaje extremadamente regular (piense que todo es una lista ); las macros le permiten tratar los datos y el código de la misma manera (no se necesitan análisis de cadenas ni otros hacks para modificar expresiones lisp). Combinas estas dos características y tienes una forma muy limpia de modificar el código.
Edición: Lo que intentaba decir es que Lisp es homoiconic , lo que significa que la estructura de datos para un programa lisp se escribe en lisp.
Entonces, terminas con una forma de crear tu propio generador de código sobre el lenguaje utilizando el lenguaje en sí con toda su potencia (por ejemplo, en Java tienes que hackear tu camino con el tejido de código de bytes, aunque algunos marcos como AspectJ te permiten hacer esto usando un enfoque diferente, es fundamentalmente un hackeo).
En la práctica, con las macros terminas construyendo tu propio mini-lenguaje encima de lisp, sin la necesidad de aprender lenguajes o herramientas adicionales, y con el uso de toda la potencia del lenguaje en sí.
Para dar una respuesta breve, las macros se utilizan para definir extensiones de sintaxis de idioma para Common Lisp o Lenguajes específicos de dominio (DSL). Estos lenguajes están incrustados directamente en el código Lisp existente. Ahora, los DSL pueden tener una sintaxis similar a Lisp (como el Prólogo Intérprete para Common Lisp de Peter Norvig) o completamente diferente (por ejemplo, Infix Notation Math para Clojure).
Aquí hay un ejemplo más concreto:
Python tiene listas de comprensión integradas en el lenguaje. Esto da una sintaxis simple para un caso común. La línea
divisibleByTwo = [x for x in range(10) if x % 2 == 0]
produce una lista que contiene todos los números pares entre 0 y 9. En los 1.5 días de Python no había tal sintaxis; Usarías algo más como esto:
divisibleByTwo = []
for x in range( 10 ):
if x % 2 == 0:
divisibleByTwo.append( x )
Ambos son funcionalmente equivalentes. Invocemos nuestra suspensión de la incredulidad y simulemos que Lisp tiene una macro de bucle muy limitada que solo hace iteración y no es una forma fácil de hacer el equivalente de comprensión de listas.
En Lisp podrías escribir lo siguiente. Debo tener en cuenta que este ejemplo creado se elige para que sea idéntico al código de Python, no un buen ejemplo de código Lisp.
;; the following two functions just make equivalent of Python''s range function
;; you can safely ignore them unless you are running this code
(defun range-helper (x)
(if (= x 0)
(list x)
(cons x (range-helper (- x 1)))))
(defun range (x)
(reverse (range-helper (- x 1))))
;; equivalent to the python example:
;; define a variable
(defvar divisibleByTwo nil)
;; loop from 0 upto and including 9
(loop for x in (range 10)
;; test for divisibility by two
if (= (mod x 2) 0)
;; append to the list
do (setq divisibleByTwo (append divisibleByTwo (list x))))
Antes de ir más lejos, debería explicar qué es una macro. Es una transformación realizada código por código. Es decir, un fragmento de código, leído por el intérprete (o compilador), que toma el código como un argumento, manipula y devuelve el resultado, que luego se ejecuta en el lugar.
Por supuesto, eso es un montón de mecanografía y los programadores son perezosos. Así que podríamos definir DSL para hacer listas de comprensión. De hecho, ya estamos usando una macro (la macro de bucle).
Lisp define un par de formas especiales de sintaxis. La comilla ( ''
) indica que el siguiente token es un literal. El quasiquote o backtick ( `
) indica que el siguiente token es un literal con escapes. Los escapes son indicados por el operador de coma. El literal ''(1 2 3)
es el equivalente de Python [1, 2, 3]
. Puedes asignarlo a otra variable o usarlo en su lugar. Puedes pensar en `(1 2 ,x)
como el equivalente de Python [1, 2, x]
donde x
es una variable definida previamente. Esta notación de lista es parte de la magia que entra en las macros. La segunda parte es el lector Lisp que sustituye de manera inteligente las macros por el código, pero se ilustra mejor a continuación:
Entonces podemos definir una macro llamada lcomp
(abreviatura de comprensión de lista). La sintaxis será exactamente igual a la python que usamos en el ejemplo [x for x in range(10) if x % 2 == 0]
- (lcomp x for x in (range 10) if (= (% x 2) 0))
(defmacro lcomp (expression for var in list conditional conditional-test)
;; create a unique variable name for the result
(let ((result (gensym)))
;; the arguments are really code so we can substitute them
;; store nil in the unique variable name generated above
`(let ((,result nil))
;; var is a variable name
;; list is the list literal we are suppose to iterate over
(loop for ,var in ,list
;; conditional is if or unless
;; conditional-test is (= (mod x 2) 0) in our examples
,conditional ,conditional-test
;; and this is the action from the earlier lisp example
;; result = result + [x] in python
do (setq ,result (append ,result (list ,expression))))
;; return the result
,result)))
Ahora podemos ejecutar en la línea de comando:
CL-USER> (lcomp x for x in (range 10) if (= (mod x 2) 0))
(0 2 4 6 8)
Bastante limpio, ¿eh? Ahora no se detiene allí. Tienes un mecanismo, o un pincel, si quieres. Puedes tener cualquier sintaxis que puedas desear. Como Python o C # ''s with
sintaxis. O la sintaxis LINQ de .NET. A fin de cuentas, esto es lo que atrae a las personas a Lisp: la máxima flexibilidad.
Piense en lo que puede hacer en C o C ++ con macros y plantillas. Son herramientas muy útiles para administrar códigos repetitivos, pero están limitadas de manera bastante severa.
- La sintaxis de macro / plantilla limitada restringe su uso. Por ejemplo, no puede escribir una plantilla que se expanda a algo que no sea una clase o una función. Las macros y las plantillas no pueden mantener fácilmente los datos internos.
- La sintaxis compleja y muy irregular de C y C ++ dificulta la escritura de macros muy generales.
Las macros Lisp y Lisp resuelven estos problemas.
- Las macros de Lisp están escritas en Lisp. Tienes todo el poder de Lisp para escribir la macro.
- Lisp tiene una sintaxis muy regular.
Habla con cualquier persona que domine C ++ y pregúntales cuánto tiempo pasaron aprendiendo toda la plantilla que necesitan para realizar la metaprogramación de plantillas. O todos los trucos locos en libros (excelentes) como Modern C ++ Design , que aún son difíciles de depurar y (en la práctica) no portátiles entre compiladores del mundo real, aunque el lenguaje se ha estandarizado durante una década. ¡Todo eso se derrite si el lenguaje que utiliza para la metaprogramación es el mismo lenguaje que utiliza para la programación!
Una macro lisp toma un fragmento de programa como entrada. Este fragmento de programa se representa como una estructura de datos que puede manipularse y transformarse de la forma que desee. Al final, la macro genera otro fragmento de programa, y este fragmento es lo que se ejecuta en tiempo de ejecución.
C # no tiene una función de macro, sin embargo, un equivalente sería si el compilador analizó el código en un árbol de CodeDOM, y lo pasó a un método, que lo transformó en otro CodeDOM, que luego se compila en IL.
Esto podría usarse para implementar la sintaxis de "azúcar" como la for each
declaración using
-clause, linq select
-expressions, etc., como macros que se transforman en el código subyacente.
Si Java tuviera macros, podría implementar la sintaxis de Linq en Java, sin necesidad de que Sun cambie el idioma base.
Aquí está el pseudocódigo para el aspecto que podría tener una macro de estilo lisp en C # para implementar el using
:
define macro "using":
using ($type $varname = $expression) $block
into:
$type $varname;
try {
$varname = $expression;
$block;
} finally {
$varname.Dispose();
}