¿Qué hace que las macros Lisp sean tan especiales?
Al leer los ensayos de Paul Graham sobre lenguajes de programación, uno podría pensar que las macros Lisp son el único camino a seguir. Como desarrollador ocupado que trabaja en otras plataformas, no he tenido el privilegio de utilizar macros Lisp. Como alguien que quiere comprender los rumores, explique qué hace que esta función sea tan poderosa.
Relacione también esto con algo que entendería del mundo del desarrollo de Python, Java, C# o C.
Para dar una respuesta breve, las macros se utilizan para definir extensiones de sintaxis de lenguaje para Common Lisp o lenguajes específicos de dominio (DSL). Estos lenguajes están integrados directamente en el código Lisp existente. Ahora, los DSL pueden tener una sintaxis similar a Lisp (como el Prolog Interpreter de Peter Norvig para Common Lisp) o completamente diferente (por ejemplo, Infix Notation Math para Clojure).
Aquí hay un ejemplo más concreto:
Python tiene listas por comprensión integradas en el lenguaje. Esto proporciona 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 días 1,5 de Python no existí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. Invoquemos nuestra suspensión de la incredulidad y pretendamos que Lisp tiene una macro de bucle muy limitada que solo realiza iteraciones y no es una manera fácil de hacer el equivalente a la comprensión de listas.
En Lisp podrías escribir lo siguiente. Debo señalar que este ejemplo artificial se eligió para que sea idéntico al código 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 continuar, debería explicar mejor 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 argumento, lo manipula y devuelve el resultado, que luego se ejecuta in situ.
Por supuesto, hay mucho que escribir y los programadores son vagos. Entonces podríamos definir DSL para hacer listas por comprensión. De hecho, ya estamos usando una macro (la macro de bucle).
Lisp define un par de formas de sintaxis especiales. La comilla ( '
) indica que el siguiente token es literal. La cuasicomilla o comilla invertida ( `
) indica que el siguiente token es un literal con escapes. Los escapes se indican mediante el operador de coma. El literal '(1 2 3)
es el equivalente al de Python [1, 2, 3]
. Puede asignarlo a otra variable o usarlo en su lugar. Puede considerarlo `(1 2 ,x)
como el equivalente de Python [1, 2, x]
donde x
hay una variable previamente definida. Esta notación de lista es parte de la magia de las macros. La segunda parte es el lector Lisp que sustituye inteligentemente el código por macros, pero que se ilustra mejor a continuación:
Entonces podemos definir una macro llamada lcomp
(abreviatura de comprensión de listas). Su sintaxis será exactamente como la de 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 bonito, ¿eh? Ahora no se detiene ahí. Tienes un mecanismo, o un pincel, si quieres. Puede tener cualquier sintaxis que desee. Como la sintaxis de Python o C# with
. O la sintaxis LINQ de .NET. Al final, esto es lo que atrae a la gente a Lisp: máxima flexibilidad.
Encontrará un debate completo 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 heurísticas y mejores conjeturas. A veces su análisis de la tasa de recorte es incorrecto y luego quiebran.
Pero Lisp es diferente. Las macros Lisp tienen acceso al analizador, y es un analizador realmente simple. A una macro Lisp no se le entrega una cadena, sino un fragmento de código fuente preparado 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.
A continuación se muestra un ejemplo ampliado. 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" al valor de la expresión "lo que sea".
Lisp también tiene listas; puede utilizar las funciones "car" y "cdr" para obtener el primer elemento de una lista o el resto de la lista, respectivamente.
¿Y ahora qué sucede si desea reemplazar el primer elemento de una lista con un nuevo valor? Existe una función estándar para hacer esto y, increíblemente, su nombre es incluso peor que "automóvil". Es "rplaca". Pero no hace falta que recuerdes "rplaca", porque puedes escribir
(setf (car somelist) whatever)
para configurar el coche de alguna lista.
Lo que realmente sucede aquí es que "setf" es una macro. En tiempo de compilación, examina sus argumentos y ve que el primero tiene la forma (car ALGO). Se dice a sí mismo: "Oh, el programador está intentando configurar el auto de algo. La función a usar para eso es 'rplaca'". Y reescribe silenciosamente el código para:
(rplaca somelist whatever)