Qu’est-ce qui rend les macros Lisp si spéciales?

En lisant les essais de Paul Graham sur les langages de programmation, on pourrait penser que les macros Lisp sont la seule solution. En tant que développeur occupé, travaillant sur d’autres plates-formes, je n’ai pas eu le privilège d’utiliser des macros Lisp. En tant que personne qui veut comprendre le buzz, veuillez expliquer ce qui rend cette fonctionnalité si puissante.

Veuillez également relier ceci à quelque chose que je comprendrais des mondes de développement de Python, de Java, de C # ou de C.

Pour donner une réponse courte, les macros sont utilisées pour définir des extensions de syntaxe de langage aux langages Common Lisp ou Domain Specific Languages ​​(DSL). Ces langages sont intégrés directement dans le code Lisp existant. Maintenant, les DSL peuvent avoir une syntaxe similaire à Lisp (comme Prolog Interpreter for Common Lisp de Peter Norvig) ou complètement différente (par exemple Infix Notation Math for Clojure).

Voici un exemple plus concret:
Python a des compréhensions de listes intégrées au langage. Cela donne une syntaxe simple pour un cas commun. La ligne

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

fournit une liste contenant tous les nombres pairs compris entre 0 et 9. De retour dans Python 1.5 jours, il n’y avait pas de syntaxe de ce type; vous utiliseriez quelque chose de plus comme ceci:

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

Celles-ci sont toutes deux fonctionnellement équivalentes. Invoquons notre suspension de l’incrédulité et faisons semblant que Lisp a une macro de boucle très limitée qui ne fait que de l’itération et pas de moyen facile de faire l’équivalent d’une compréhension de liste.

Dans Lisp, vous pouvez écrire ce qui suit. Je devrais noter que cet exemple artificiel est choisi pour être identique au code Python et non pas un bon exemple de code 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)))) 

Avant d’aller plus loin, je devrais mieux expliquer ce qu’est une macro. C’est une transformation effectuée sur le code par code. Autrement dit, un morceau de code, lu par l’interpréteur (ou le compilateur), qui prend le code en tant qu’argument, manipule et renvoie le résultat, qui est ensuite exécuté sur place.

Bien sûr, c’est beaucoup de dactylographie et les programmeurs sont paresseux. Nous pourrions donc définir DSL pour faire des listes compréhensibles. En fait, nous utilisons déjà une macro (la macro de boucle).

Lisp définit deux formes de syntaxe spéciales. Le guillemet ( ' ) indique que le jeton suivant est un littéral. Le quasiquote ou backtick ( ` ) indique que le jeton suivant est un littéral avec des échappements. Les échappements sont indiqués par l’opérateur de virgule. Le littéral '(1 2 3) est l’équivalent de celui de Python [1, 2, 3] . Vous pouvez l’assigner à une autre variable ou l’utiliser. Vous pouvez penser à `(1 2 ,x) comme l’équivalent de Python [1, 2, x]x est une variable définie précédemment. Cette notation de liste fait partie de la magie qui entre dans les macros. La deuxième partie est le lecteur Lisp qui remplace intelligemment les macros par du code, mais cela est mieux illustré ci-dessous:

On peut donc définir une macro appelée lcomp (abréviation de compréhension de la liste). Sa syntaxe sera exactement celle du python que nous avons utilisé dans l’exemple [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))) 

Maintenant, nous pouvons exécuter sur la ligne de commande:

 CL-USER> (lcomp x for x in (range 10) if (= (mod x 2) 0)) (0 2 4 6 8) 

Joli, hein? Maintenant, ça ne s’arrête pas là. Vous avez un mécanisme, ou un pinceau, si vous voulez. Vous pouvez avoir n’importe quelle syntaxe possible. Comme Python ou C # with syntaxe. Ou la syntaxe LINQ de .NET. En fin de compte, c’est ce qui attire les gens vers Lisp – la flexibilité ultime.

Vous trouverez un débat complet sur la macro lisp ici .

Un sous-ensemble intéressant de cet article:

Dans la plupart des langages de programmation, la syntaxe est complexe. Les macros doivent démonter la syntaxe du programme, l’parsingr et la réassembler. Ils n’ont pas access à l’parsingur du programme, ils doivent donc dépendre des heuristiques et des meilleures hypothèses. Parfois, leur parsing à la carte est erronée, et ensuite ils se brisent.

Mais Lisp est différent. Les macros Lisp ont access à l’parsingur, et c’est un parsingur très simple. Une macro Lisp ne reçoit pas de chaîne, mais un morceau de code source préparté sous la forme d’une liste, car la source d’un programme Lisp n’est pas une chaîne; c’est une liste. Et les programmes Lisp sont vraiment efficaces pour démonter les listes et les rassembler. Ils le font de manière fiable, chaque jour.

Voici un exemple étendu. Lisp a une macro, appelée “setf”, qui effectue une affectation. La forme la plus simple de setf est

  (setf x whatever) 

qui définit la valeur du symbole “x” à la valeur de l’expression “quel que soit”.

Lisp a aussi des listes; vous pouvez utiliser les fonctions “car” et “cdr” pour obtenir respectivement le premier élément d’une liste ou le rest de la liste.

Maintenant, que faire si vous voulez remplacer le premier élément d’une liste par une nouvelle valeur? Il existe une fonction standard pour faire cela, et incroyablement, son nom est encore pire que “car”. C’est “Rplaca”. Mais vous ne devez pas vous rappeler “Rplaca”, car vous pouvez écrire

  (setf (car somelist) whatever) 

mettre la voiture de somelist.

Ce qui se passe vraiment ici, c’est que “setf” est une macro. Au moment de la compilation, il examine ses arguments et voit que le premier a la forme (car SOMETHING). Il se dit “Oh, le programmeur essaie de régler la voiture de quelque chose. La fonction à utiliser pour cela est” rplaca “.” Et il réécrit discrètement le code en place pour:

  (rplaca somelist whatever) 

Les macros Lisp communes étendent essentiellement les “primitives syntaxiques” de votre code.

Par exemple, dans C, la construction switch / case ne fonctionne qu’avec les types intégraux et si vous voulez l’utiliser pour des flottants ou des chaînes, vous devez utiliser des instructions if nestedes et des comparaisons explicites. Il est également impossible d’écrire une macro C pour faire le travail à votre place.

Mais, comme une macro lisp est (essentiellement) un programme lisp qui prend des extraits de code comme entrée et retourne du code pour remplacer “l’invocation” de la macro, vous pouvez étendre votre répertoire “primitives” autant que vous le souhaitez, finissant généralement par avec un programme plus lisible.

Pour faire la même chose en C, vous devez écrire un pré-processeur personnalisé qui mange votre source initiale (pas tout à fait C) et crache quelque chose qu’un compilateur C peut comprendre. Ce n’est pas une mauvaise façon de procéder, mais ce n’est pas nécessairement la plus facile.

Les macros Lisp vous permettent de décider quand (ou pas du tout) une partie ou une expression sera évaluée. Pour donner un exemple simple, pensez à C:

 expr1 && expr2 && expr3 ... 

Ce que cela expr1 , c’est: Évaluez expr1 et, si cela est vrai, évaluez expr2 , etc.

Maintenant, essayez de faire cela && dans une fonction … c’est bien, vous ne pouvez pas. Appeler quelque chose comme:

 and(expr1, expr2, expr3) 

Évaluer les trois exprs avant de donner une réponse, que expr1 soit fausse ou non!

Avec les macros Lisp, vous pouvez coder quelque chose comme:

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

maintenant vous avez un && , que vous pouvez appeler comme une fonction et qui n’évaluera pas les formes que vous lui passez, à moins qu’elles ne soient toutes vraies.

Pour voir comment cela est utile, contrastez:

 (&& (very-cheap-operation) (very-expensive-operation) (operation-with-serious-side-effects)) 

et:

 and(very_cheap_operation(), very_expensive_operation(), operation_with_serious_side_effects()); 

Les macros permettent également de créer de nouveaux mots-clés et / ou des mini-langages (consultez la macro (loop ...) pour un exemple), en intégrant d’autres langages dans lisp, par exemple, vous pourriez écrire une macro dire quelque chose comme:

 (setvar *rows* (sql select count(*) from some-table where column1 = "Yes" and column2 like "some%ssortingng%") 

Et ce n’est même pas entrer dans les macros du lecteur .

J’espère que cela t’aides.

Je ne pense pas avoir déjà vu les macros Lisp mieux expliquées que par ce camarade: http://www.defmacro.org/ramblings/lisp.html

Une macro lisp prend un fragment de programme en entrée. Ce fragment de programme est représenté par une structure de données qui peut être manipulée et transformée comme vous le souhaitez. À la fin, la macro génère un autre fragment de programme et ce fragment est ce qui est exécuté à l’exécution.

C # n’a pas de fonction de macro, mais un équivalent serait si le compilateur analysait le code dans une arborescence CodeDOM, et le transmettait à une méthode, qui la transformait en un autre CodeDOM, qui est ensuite compilé en IL.

Cela pourrait être utilisé pour implémenter une syntaxe “sucre” comme for each déclaration en using -clause, linq select -expressions et ainsi de suite, comme des macros qui se transforment dans le code sous-jacent.

Si Java avait des macros, vous pourriez implémenter la syntaxe Linq en Java, sans avoir besoin de Sun pour changer le langage de base.

Voici un pseudo-code sur l’apparence d’une macro de style lisp en C # pour l’implémentation:

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

Pensez à ce que vous pouvez faire en C ou C ++ avec des macros et des modèles. Ce sont des outils très utiles pour gérer du code répétitif, mais ils sont limités de manière assez sévère.

  • La syntaxe limitée des macros / modèles limite leur utilisation. Par exemple, vous ne pouvez pas écrire un modèle qui se développe en une classe ou une fonction. Les macros et les modèles ne peuvent pas facilement gérer les données internes.
  • La syntaxe complexe et très irrégulière de C et C ++ rend difficile l’écriture de macros très générales.

Les macros Lisp et Lisp résolvent ces problèmes.

  • Les macros Lisp sont écrites en Lisp. Vous avez toute la puissance de Lisp pour écrire la macro.
  • Lisp a une syntaxe très régulière.

Parlez à tous ceux qui maîsortingsent C ++ et demandez-leur combien de temps ils ont passé à apprendre tous les modèles nécessaires à la métaprogrammation des modèles. Ou tous les trucs fous des (excellents) livres comme Modern C ++ Design , qui sont toujours difficiles à déboguer et (en pratique) non-portables entre des compilateurs réels, même si le langage a été normalisé pendant une décennie. Tout cela disparaît si la langue utilisée pour la métaprogrammation est la même que celle utilisée pour la programmation!

Je ne suis pas sûr de pouvoir donner un aperçu des publications (excellentes) de chacun, mais …

Les macros Lisp fonctionnent parfaitement grâce à la syntaxe Lisp.

Lisp est un langage extrêmement régulier (pensez à tout est une liste ); Les macros vous permettent de traiter les données et le code de la même manière (aucune parsing de chaîne ou autre piratage n’est nécessaire pour modifier les expressions lisp). Vous combinez ces deux fonctionnalités et vous avez un moyen très propre de modifier le code.

Edit: Ce que j’essayais de dire, c’est que Lisp est homoiconique , ce qui signifie que la structure de données d’un programme lisp est écrite en lisp même.

Donc, vous vous retrouvez avec un moyen de créer votre propre générateur de code sur le langage en utilisant le langage lui-même avec toute sa puissance (par exemple, en Java, vous devez pirater avec le tissage bytecode, bien que certains framework faites-le en utilisant une approche différente, c’est fondamentalement un hack).

En pratique, avec les macros, vous créez votre propre mini-langage par-dessus tout, sans avoir à apprendre de langues ou d’outils supplémentaires, et à utiliser toute la puissance du langage.

Étant donné que les réponses existantes donnent de bons exemples concrets expliquant ce que les macros réalisent et comment, peut-être cela aiderait-il à rassembler certaines des idées sur la raison pour laquelle la macro-facilité est un gain significatif par rapport aux autres langues ; d’abord de ces réponses, puis un grand d’ailleurs:

… en C, il faudrait écrire un pré-processeur personnalisé [qui serait probablement considéré comme un programme C suffisamment compliqué ] …

– Vatine

Parlez à toute personne maîsortingsant C ++ et demandez-leur combien de temps ils ont passé à apprendre tout le fudgery de modèle dont ils ont besoin pour faire de la métaprogrammation de modèles [qui n’est toujours pas aussi puissant].

– Matt Curtis

… en Java, vous devez vous frayer un chemin avec le tissage bytecode, bien que certains frameworks comme AspectJ vous permettent de le faire en utilisant une approche différente, c’est fondamentalement un hack.

– Miguel Ping

DOLIST est similaire à celui de Perl ou à celui de Python. Java a ajouté une sorte de construction de boucle avec le “amélioré” pour la boucle dans Java 1.5, dans le cadre de JSR-201. Notez ce que font les macros de différence. Un programmeur Lisp qui remarque un modèle commun dans son code peut écrire une macro pour se donner une abstraction de ce modèle au niveau de la source. Un programmeur Java qui remarque le même modèle doit convaincre Sun que cette abstraction particulière mérite d’être ajoutée à la langue. Ensuite, Sun doit publier un JSR et convoquer un «groupe d’experts» à l’échelle de l’indussortinge pour tout comprendre. Selon Sun, ce processus prend en moyenne 18 mois. Après cela, les auteurs du compilateur doivent tous mettre à niveau leurs compilateurs pour prendre en charge la nouvelle fonctionnalité. Et même une fois que le compilateur favori du programmeur Java prend en charge la nouvelle version de Java, ils ne peuvent probablement pas utiliser la nouvelle fonctionnalité tant qu’ils ne sont pas autorisés à rompre la compatibilité avec les anciennes versions de Java. Ainsi, un désagrément que les programmeurs Common Lisp peuvent résoudre eux-mêmes en moins de cinq minutes sévit depuis des années chez les programmeurs Java.

– Peter Seibel, dans “Practical Common Lisp”

Les macros Lisp représentent un modèle qui se produit dans presque tous les projets de programmation importants. Finalement, dans un grand programme, vous avez une certaine section de code où vous réalisez qu’il serait plus simple et moins enclin aux erreurs d’écrire un programme qui génère du code source sous forme de texte que vous pouvez ensuite simplement coller.

Dans Python, les objects ont deux méthodes __repr__ et __str__ . __str__ est simplement la représentation lisible par l’homme. __repr__ renvoie une représentation qui est un code Python valide, c’est-à-dire quelque chose qui peut être entré dans l’interpréteur en tant que Python valide. De cette façon, vous pouvez créer de petits extraits de Python qui génèrent un code valide pouvant être collé dans votre source réelle.

Dans Lisp, tout ce processus a été formalisé par le macro-système. Bien sûr, cela vous permet de créer des extensions de la syntaxe et de faire toutes sortes de choses fantaisistes, mais son utilité réelle est résumée par ce qui précède. Bien sûr, le système macro Lisp vous permet de manipuler ces “extraits” avec toute la puissance du langage.

En bref, les macros sont des transformations de code. Ils permettent d’introduire de nombreuses nouvelles constructions syntaxiques. Par exemple, considérez LINQ en C #. Dans lisp, il existe des extensions de langage similaires qui sont implémentées par des macros (par exemple, une construction de boucle intégrée, iterate). Les macros réduisent considérablement la duplication de code. Les macros permettent d’incorporer des «petits langages» (par exemple, dans c # / java, on utiliserait xml pour configurer, dans lisp on peut obtenir la même chose avec les macros). Les macros peuvent masquer les difficultés d’utilisation des bibliothèques.

Par exemple, en lisp vous pouvez écrire

 (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)) 

et cela cache toutes les choses de la firebase database (transactions, fermeture de connexion correcte, récupération de données, etc.) alors qu’en C # cela nécessite de créer SqlConnections, SqlCommands, d’append SqlParameters à SqlCommands, de boucler sur SqlDataReaders.

Bien que tout ce qui précède explique ce que sont les macros et même des exemples intéressants, je pense que la principale différence entre une macro et une fonction normale est que LISP évalue d’abord tous les parameters avant d’appeler la fonction. Avec une macro c’est l’inverse, LISP transmet les parameters non évalués à la macro. Par exemple, si vous passez (+ 1 2) à une fonction, la fonction recevra la valeur 3. Si vous la transmettez à une macro, elle recevra une liste (+ 1 2). Cela peut être utilisé pour faire toutes sortes de choses incroyablement utiles.

  • Ajouter une nouvelle structure de contrôle, par exemple une boucle ou la déconstruction d’une liste
  • Mesurer le temps qu’il faut pour exécuter une fonction passée. Avec une fonction, le paramètre serait évalué avant que le contrôle ne soit transmis à la fonction. Avec la macro, vous pouvez épisser votre code entre le début et l’arrêt de votre chronomètre. Le ci-dessous a exactement le même code dans une macro et une fonction et la sortie est très différente. Remarque: Ceci est un exemple artificiel et l’implémentation a été choisie de manière à ce qu’elle soit identique pour mieux mettre en évidence la différence.

     (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 

Je l’ai eu du livre de recettes The Common Lisp, mais je pense que cela explique pourquoi les macros Lisp sont bonnes.

“Une macro est un morceau de code Lisp ordinaire qui fonctionne sur un autre morceau de code Lisp putatif, le traduisant en (une version plus proche de) Lisp exécutable. Cela peut paraître un peu compliqué, alors donnons un exemple simple. version de setq qui définit deux variables à la même valeur. Donc, si vous écrivez

 (setq2 xy (+ z 3)) 

quand z=8 x et y sont tous deux mis à 11. (Je ne peux penser à aucune utilisation pour cela, mais c’est juste un exemple.)

Il devrait être évident que nous ne pouvons pas définir setq2 comme une fonction. Si x=50 et y=-5 , cette fonction recevrait les valeurs 50, -5 et 11; il n’aurait aucune connaissance des variables à définir. Ce que nous voulons vraiment dire, c’est que lorsque vous (le système Lisp) voyez (setq2 v1 v2 e) , le traiter comme équivalent à (progn (setq v1 e) (setq v2 e)) . En fait, ce n’est pas tout à fait correct, mais ça va faire pour le moment. Une macro nous permet de faire précisément ceci, en spécifiant un programme pour transformer le modèle d’entrée (setq2 v1 v2 e) “en modèle de sortie (progn ...) “.

Si vous pensiez que c’était bien, vous pouvez continuer à lire ici: http://cl-cookbook.sourceforge.net/macros.html

En python, vous avez des décorateurs, vous avez essentiellement une fonction qui prend une autre fonction en entrée. Vous pouvez faire ce que vous voulez: appeler la fonction, faire autre chose, envelopper l’appel de fonction dans une version d’acquisition de ressource, etc., mais vous ne pouvez pas voir à l’intérieur de cette fonction. Disons que nous voulions le rendre plus puissant, disons que votre décorateur a reçu le code de la fonction sous forme de liste, vous pouvez non seulement exécuter la fonction telle quelle, mais vous pouvez maintenant en exécuter certaines parties, réorganiser les lignes de la fonction, etc.