2008-11-06 17 views
246

Leyendo Paul Graham's essays en lenguajes de programación uno pensaría que Lisp macros es el único camino a seguir. Como desarrollador ocupado, trabajando en otras plataformas, no he tenido el privilegio de usar macros Lisp. Como alguien que quiere entender el rumor, explique qué hace que esta característica sea tan poderosa.¿Qué hace que las macros de Lisp sean tan especiales?

Por favor, también relacione esto con algo que entendería de los mundos del desarrollo de Python, Java, C# o C.

+2

Por cierto, hay un procesador de macros de estilo LISP para C# llamado Lemp: http://ecsharp.net/lemp/ ... JavaScript también ha llamado Sweet.js: https://www.sweetjs.org/ – Qwertie

+0

@Qwertie ¿Los sweetjs incluso funcionan en estos días? –

+0

No lo he usado, pero el compromiso más reciente fue hace seis meses ... ¡lo suficientemente bueno para mí! – Qwertie

Respuesta

91

Encontrará un amplio debate en torno al lisp macro here.

Un subgrupo interesante de dicho artículo:

En la mayoría de los lenguajes de programación, la sintaxis es compleja. Las macros deben separar la sintaxis del programa, analizarla y volver a montarla. No tienen acceso al analizador del programa, por lo que deben depender de la heurística y las mejores suposiciones. A veces su análisis de tasa de corte es incorrecto, y luego se rompen.

Pero Lisp es diferente. Lisp macros do tienen acceso al analizador, y es un analizador realmente simple. Una macro Lisp no recibe una cadena, sino un fragmento de código fuente 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 separar listas y volverlas a armar. Lo hacen de manera confiable, 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 fija el valor del símbolo "x" para el valor de la expresión "lo que sea".

Lisp también tiene listas; puede usar las funciones "coche" y "cdr" para obtener el primer elemento de una lista o el resto de la lista, respectivamente.

¿Y ahora qué pasa si quiere 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 "rplaca". Pero usted no tiene que recordar "rplaca", porque se puede escribir

(setf (car somelist) whatever) 

para fijar el coche de algunalista.

Lo que realmente está sucediendo aquí es que "setf" es una macro. En tiempo de compilación, examina sus argumentos, y ve que el primero tiene la forma (auto ALGO). Se dice a sí mismo "Oh, el programador está tratando de configurar el auto de algo. La función para usar es 'rplaca'". Y en silencio vuelve a escribir el código en lugar de:

(rplaca somelist whatever) 
+3

setf es una bonita ilustración del poder de las macros, gracias por incluirlo. – Joel

36

macros Lisp le permiten decidir cuándo (en su caso) serán evaluados cualquier parte o expresión. Para poner un ejemplo sencillo, pensar en C de:

expr1 && expr2 && expr3 ... 

Lo que esto dice es: Evaluar expr1, y, en caso de ser cierto, evaluar expr2, etc.

Ahora tratará de hacer esta && en una función ... eso es correcto, no puedes.Llamar a algo como:

and(expr1, expr2, expr3) 

evaluará todas las tres exprs antes de ceder una respuesta independientemente de si expr1 era falso!

con macros Lisp puede codificar algo como:

(defmacro && (expr1 &rest exprs) 
    `(if ,expr1      ;` Warning: I have not tested 
     (&& ,@exprs)    ; this and might be wrong! 
     nil)) 

ahora usted tiene una &&, que se puede llamar como una función y no va a evaluar cualquier forma se pasan a no ser que todos ellos son cierto.

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 están creando nuevas palabras clave y/o mini-idiomas (echa un vistazo a la (loop ...) macro para un ejemplo), integrando otros idiomas en lisp, por ejemplo, puede 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 entra en Reader macros.

Espero que esto ayude.

+0

Creo que debería ser: "(aplicar &&, @ exprs); ¡esto y podría estar equivocado!" – Svante

+1

@svante - en dos aspectos: primero, && es una macro, no una función; aplicar solo funciona en funciones. segundo, aplicar toma una lista de argumentos para pasar, entonces quieres uno de "(funcall fn, @ exprs)", "(aplica fn (list, @ exprs)" o "(apply fn, @ exprs nil)", no "(aplicar fn, @ exprs)". – Aaron

+0

'(y ...' evaluará las expresiones hasta que se evalúe como falso, tenga en cuenta que se producirán los efectos secundarios generados por la evaluación falsa, solo se omitirán las expresiones siguientes. – ocodo

46

Las macros Common Lisp esencialmente amplían las "primitivas sintácticas" de su código.

Por ejemplo, en C, la construcción switch/case solo funciona con tipos integrales y si desea usarla para flotantes o cadenas, se le dejan declaraciones if anidadas y comparaciones explícitas. Tampoco hay manera de que pueda escribir una macro C para hacer el trabajo por usted.

Pero, como 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" tanto como desee , generalmente terminando con un programa más legible.

Para hacer lo mismo en C, tendría que escribir un preprocesador personalizado que consuma su fuente inicial (no bastante C) y escuche algo que un compilador de C pueda comprender. No es una forma incorrecta de hacerlo, pero no es necesariamente la más fácil.

10

Una macro de 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 facilidad macro, sin embargo, un equivalente sería si el compilador analizó el código en un árbol de códigosDOM y lo pasó a un método que lo transformó en otro CodeDOM, que luego se compila en IL.

esto podría ser utilizado para poner en práctica el "azúcar" sintaxis como la for each -statement using -clause, LINQ select -expresiones y así sucesivamente, como macros que se transforma en el código subyacente.

Si Java tuviera macros, podría implementar la sintaxis Linq en Java, sin necesidad de que Sun cambie el idioma base.

Aquí es pseudo-código para la forma de una macro-estilo Lisp en C# para implementar using podía mirar:

define macro "using": 
    using ($type $varname = $expression) $block 
into: 
    $type $varname; 
    try { 
     $varname = $expression; 
     $block; 
    } finally { 
     $varname.Dispose(); 
    } 
9

Piense en lo que puede hacer en C o C++ con las macros y plantillas. Son herramientas muy útiles para administrar código repetitivo, pero están limitadas de manera bastante severa.

  • La sintaxis limitada de macro/plantilla 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, muy irregular de C y C++ hace que sea difícil escribir macros muy generales.

Las macros Lisp y Lisp resuelven estos problemas.

  • Lisp macros están escritos en Lisp. Tienes todo el poder de Lisp para escribir la macro.
  • Lisp tiene una sintaxis muy regular.

Hable con cualquier persona que domine C++ y pregúntele cuánto tiempo pasó aprendiendo toda la plantilla de plantilla que necesitan para hacer la metaprogramación de la plantilla. O todos los trucos locos en libros (excelentes) como Modern C++ Design, que todavía son difíciles de depurar y (en la práctica) no portátiles entre compiladores del mundo real, aunque el lenguaje ha sido estandarizado durante una década. ¡Todo eso se desvanecerá si el lenguaje que utilizas para la metaprogramación es el mismo que usas para programar!

+8

Bueno, para ser justos, el problema con la metaprogramación de plantillas de C++ no es que el lenguaje de metaprogramación sea * diferente *, sino que es horrible: no se diseñó tanto como se descubrió en lo que se pretendía que fuera una funcionalidad de plantilla mucho más simple. –

+0

@Brooks Claro. Las características emergentes no siempre son malas. Desafortunadamente, en un lenguaje lento y movido por un comité, es difícil solucionarlo cuando lo hace. Es una pena que muchas de las funciones nuevas y útiles de C++ estén escritas en un lenguaje que pocos pueden leer, y hay una gran brecha entre el p promedio rogrammer y el "sumo sacerdote". –

+2

@downvoter: si hay algún problema con mi respuesta, por favor deje un comentario para que todos podamos compartir el conocimiento. –

7

No estoy seguro que puedo añadir una cierta penetración a (excelente) los mensajes de todos, pero ...

macros Lisp gran trabajo debido a la naturaleza Lisp sintaxis.

Lisp es un lenguaje extremadamente regular (piensa en todo es una lista ); macros le permite tratar los datos y el código como si fueran iguales (no se necesitan análisis sintácticos u otros hacks para modificar las expresiones de lisp). Combina estas dos características y tiene una manera muy de limpiar para modificar el código.

Editar: Lo que estaba tratando de decir es que Lisp es homoiconic, lo que significa que la estructura de datos para un programa Lisp está escrito en Lisp en sí.

Así que terminas con una forma de crear tu propio generador de códigos sobre el idioma usando el lenguaje mismo con toda su potencia (por ejemplo, en Java tienes que hackear tu camino con bytecode weaving, aunque algunos frameworks como AspectJ te permite hacer esto usando un enfoque diferente, es fundamentalmente un truco).

En la práctica, con macros que terminan la construcción de su propia mini- idioma en la parte superior de Lisp, sin la necesidad de aprender idiomas o herramientas adicionales, y con el uso de toda la potencia de la propia lengua.

5

En resumen, las macros son transformaciones de código. Permiten introducir muchos nuevos constructos de sintaxis. Por ejemplo, considere LINQ en C#. En lisp, hay extensiones de lenguaje similares que se implementan mediante macros (por ejemplo, constructor de bucle incorporado, iterar).Las macros disminuyen significativamente la duplicación de código. Las macros permiten incrustar «pequeños lenguajes» (por ejemplo, donde en C#/java uno usaría xml para configurar, en lisp lo mismo se puede lograr con macros). Las macros pueden ocultar las dificultades de usar bibliotecas.

Por ejemplo, en Lisp puede 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 disimular todo el material de base de datos (transacciones, cierre la conexión correcta, los datos de recogida, etc.), mientras que en C# esto requiere la creación de SqlConnections, SqlCommands, SqlParameters añadiendo a SqlCommands, haciendo un bucle en SqlDataReaders, cerrándolos adecuadamente.

6

Lisp macros representa un patrón que se produce en casi cualquier proyecto de programación importante. Eventualmente, en un programa grande, tiene una determinada sección de código donde se da cuenta de que sería más simple y menos propenso a errores escribir un programa que emita código fuente como texto que luego puede pegar.

En objetos Python tiene dos métodos __repr__ y __str__. __str__ es simplemente la representación humana legible. __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 un Python válido. De esta forma, puede crear pequeños fragmentos de Python que generen código válido que pueda pegarse en su fuente real.

En Lisp todo este proceso ha sido formalizado por el macro sistema. Claro que le permite crear extensiones a la sintaxis y hacer todo tipo de cosas sofisticadas, pero su utilidad real se resume en lo anterior. Por supuesto, ayuda que el sistema macro Lisp le permita manipular estos "fragmentos" con toda la potencia de todo el lenguaje.

+1

Su primer párrafo es muy claro para un forastero Lisp, que es importante. – Wildcard

244

para dar la respuesta definitiva, las macros se utilizan para definir las extensiones de la sintaxis del lenguaje de Common Lisp o de dominio de idiomas específicos (DSL). Estos idiomas están incrustados en el código Lisp existente. Ahora, las 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 de comprensiones 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 Python 1.5 días no había tal sintaxis; usted usaría algo más así:

divisibleByTwo = [] 
for x in range(10): 
    if x % 2 == 0: 
     divisibleByTwo.append(x) 

Ambos son funcionalmente equivalentes. Invocamos nuestra suspensión de la incredulidad y pretendemos que Lisp tiene una macro de bucle muy limitada que solo hace iteración y no es una manera fácil de hacer el equivalente a la lista de comprensión.

En Lisp puede escribir lo siguiente. Debo señalar que este ejemplo artificial se escogió para ser 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 ir más allá, debería explicar mejor qué es una macro. Es una transformación realizada en el código por el 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 en el lugar.

Por supuesto que es mucha mecanografía y los programadores son flojos. Entonces 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 de sintaxis especiales. La cita (') indica que el siguiente token es un literal. El cuasiquote 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 al [1, 2, 3] de Python. Puede 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 previamente definida. Esta notación de lista es parte de la magia que entra en las macros. La segunda parte es el lector Lisp que inteligentemente sustituye las macros por el código, pero eso se ilustra mejor a continuación:

Así que podemos definir una macro llamada lcomp (abreviatura de comprensión de la lista). Es la sintaxis será exactamente igual que la pitón que hemos utilizado 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 comandos:

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. Puede tener cualquier sintaxis que pueda desear. Como la sintaxis de Python o C# with. O la sintaxis LINQ de .NET. En resumen, esto es lo que atrae a las personas a Lisp: máxima flexibilidad.

+27

+1 para implementar la comprensión de la lista en Lisp, porque ¿por qué no? – ckb

+4

@ckb En realidad, LISP ya tiene una macro de comprensión de lista en la biblioteca estándar: '(loop para x de 0 debajo de 10 cuando (evenp x) collect x)', [más ejemplos aquí] (http://www.unixuser.org /~euske/doc/cl/loop.html). Pero, de hecho, el bucle es "solo un macro" (de hecho [lo reimpleé desde el principio]] (https://github.com/dumbs/2010-m1s1-compilation/blob/master/bootstrap/26-loop .lisp)) –

+6

Sé que no tiene mucha relación, pero me pregunto acerca de la sintaxis y cómo funciona el análisis ... Digamos que llamo a lcomp de esa manera (cambiando el elemento thirs de "para" a "azertyuiop"): (lcomp x azertyuiop x in (rango 10) if (= (% x 2) 0)) ¿funcionará la macro como se esperaba? ¿O es el parámetro "para" utilizado en el ciclo de modo que debe ser la cadena "para" cuando se llama? – dader

-2

En python tienes decoradores, básicamente tienes una función que toma otra función como entrada. Puedes hacer lo que quieras: llamar a la función, hacer otra cosa, ajustar la llamada a la función en un lanzamiento de adquisición de recursos, etc. pero no puedes 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 como está, sino que ahora puede ejecutar partes de ella, reordenar líneas de la función, etc.

0

Obtuve esto del libro de cocina Common Lisp, pero creo que explica por qué las macros de lisp son buenas de una manera agradable.

"Una macro es una pieza ordinaria de código Lisp que opera en otra parte del supuesto código Lisp, traduciéndolo en (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 desea una versión de setq que establece dos variables al mismo valor. Así que si usted escribe

(setq2 x y (+ z 3)) 

cuando z=8 tanto x como y se ponen a 11. (no puedo pensar en 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 se suponía que debían establecerse. Lo que realmente queremos decir es que cuando usted (el sistema Lisp) vea (setq2 v1 v2 e), trátelo como equivalente a (progn (setq v1 e) (setq v2 e)). En realidad, esto no es del todo correcto, 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 pensaron que esto era agradable se puede seguir leyendo aquí: http://cl-cookbook.sourceforge.net/macros.html

+1

Es posible definir 'setq2' como una función si' x' y 'y' se pasan por referencia. No sé si es posible en CL, sin embargo. Entonces, para alguien que no conoce Lisps o CL en particular, este no es un ejemplo muy ilustrativo IMO – neoascetic

+0

@neoascetic La aprobación de argumentos CL es solo por valor (por eso necesita macros en primer lugar). algunos valores son punteros (como las listas). –

6

Dado que las respuestas existentes dan buenos ejemplos concretos que explican lo que logran las macros y cómo, tal vez ayudarían a recoger a algunos de los pensamientos sobre por qué la instalación de macro es una ganancia significativa en relación con otras lenguas; primera de estas respuestas, entonces un gran uno de otro lugar:

... en C, que tendrían que escribir un pre personalizada -procesador [que probablemente calificaría como sufficiently complicated C program] ...

Vatine

hablar con cualquier persona que ha dominado C++ y preguntarles cuánto tiempo pasaron aprendiendo todo el Fudgery plantilla que tienen que hacer la plantilla metaprogramming [que todavía no es tan potente ]

Matt Curtis

... en Java que tiene que abrirse paso con el tejido de código de bytes, aunque algunos marcos como AspectJ le permite hacer esto usando un enfoque diferente, es fundamentalmente un truco.

Miguel Ping

DOLIST es similar a foreach de Perl o Python para. Java agregó un tipo similar de construcción de bucle con el bucle "mejorado" para Java 1.5, como parte de JSR-201. Observe qué diferencia hacen las macros. Un programador de Lisp que advierte un patrón común en su código puede escribir una macro para darse una abstracción de nivel de origen de ese patrón. Un programador de Java que advierta el mismo patrón tiene que convencer a Sun de que esta abstracción particular vale la pena agregar al lenguaje. Entonces, Sun tiene que publicar un JSR y convocar a un "grupo de expertos" de toda la industria para resolverlo todo. Ese proceso - de acuerdo con Sun - toma un promedio de 18 meses. Después de eso, todos los escritores del compilador deben actualizar sus compiladores para admitir la nueva característica. E incluso una vez que el compilador favorito del programador de Java admite la nueva versión de Java, es probable que "todavía" no puedan usar la nueva característica hasta que no puedan romper la compatibilidad con versiones anteriores de Java. Entonces, una molestia que los programadores de Common Lisp pueden resolver por sí mismos en cinco minutos afecta a los programadores de Java por años.

Peter Seibel, in "Practical Common Lisp"

2

Mientras que el por encima de todo se explica qué macros se e incluso tienen ejemplos interesantes, creo que la diferencia clave entre una macro y una función normal es que LISP evalúa todos los parámetros primeros antes de llamar a la función . Con una macro es al revés, LISP pasa los parámetros no evaluados 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 usar para hacer todo tipo de cosas increíblemente útiles.

  • Añadiendo una nueva estructura de control, p.bucle o la deconstrucción de una lista
  • Mida el tiempo que lleva ejecutar una función transferida. Con una función, el parámetro se evaluaría antes de pasar el control a la función. Con el macro, puede empalmar su código entre el inicio y la detención de su cronómetro. El siguiente tiene exactamente el mismo código 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ó para que sea idéntico 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 
    
+0

Por cierto, Scala ha agregado macros al idioma. Si bien carecen de la belleza de las macros de Lisp porque el lenguaje no es homoicónico, definitivamente vale la pena investigarlo y los árboles sintácticos abstractos que proporcionan pueden ser más fáciles de usar al final. Es muy temprano para decir qué macro sistema prefiero. –

+0

* "LISP pasa los parámetros no evaluados a la macro" * finalmente una respuesta que lo dice claramente. pero se olvidó de la segunda mitad de eso: * y el resultado de una macro es un código transformado que el sistema evaluará en su totalidad en lugar del original como si estuviera allí en primer lugar * (a menos que sea una vez más una llamar a una macro, que también se transformará, por * esa * macro esta vez). –

Cuestiones relacionadas