web-dev-qa-db-fra.com

Qu'est-ce qui rend les macros LISP si spéciales?

En lisant essais de Paul Graham sur les langages de programmation, on pourrait penser que macros LISP est 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 désireuse de comprendre le buzz, veuillez expliquer en quoi cette fonctionnalité est si puissante.

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

282
minty

Pour donner une réponse courte, les macros sont utilisées pour définir les extensions de syntaxe de langage pour Common LISP ou Domain Specific Languages ​​(DSL). Ces langues sont directement intégrées au code LISP existant. Maintenant, les DSL peuvent avoir une syntaxe similaire à celle de LISP (comme celle de Peter Norvig interprète Prolog pour Common LISP) ou complètement différente (par exemple Infix Notation Math pour Clojure).

Voici un exemple plus concret:
Python a une compréhension de liste intégrée au langage. Cela donne une syntaxe simple pour un cas commun. La ligne

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

donne une liste contenant tous les nombres pairs compris entre 0 et 9. De retour dans les Python 1.5 , il n’existait pas de telle syntaxe; vous utiliseriez quelque chose de plus semblable à ceci:

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

Ce sont tous deux fonctionnellement équivalents. Appelons notre suspension d'incrédulité et supposons que LISP a une macro de boucle très limitée qui ne fait que l'itération et qu'il n'existe 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) n'est 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. C'est-à-dire qu'un morceau de code, lu par l'interpréteur (ou le compilateur), qui prend le code en tant qu'argument, manipule et retourne le résultat, qui est ensuite exécuté sur place.

Bien sûr, cela fait beaucoup de dactylographie et les programmeurs sont paresseux. Nous pourrions donc définir DSL pour faire de la compréhension de liste. En fait, nous utilisons déjà une macro (la macro en boucle).

LISP définit deux formes de syntaxe spéciales. La citation (') Indique que le prochain jeton est un littéral. La quasiquote ou le backtick (`) Indique que le prochain jeton est un littéral avec des échappements. Les échappements sont indiqués par l'opérateur virgule. Le littéral '(1 2 3) est l'équivalent de [1, 2, 3] De Python. Vous pouvez l'affecter à une autre variable ou l'utiliser à la place. Vous pouvez penser à `(1 2 ,x) comme à l'équivalent de [1, 2, x] De Python, où x est une variable définie précédemment. Cette notation de liste fait partie de la magie qui va dans les macros. La deuxième partie est le lecteur LISP qui substitue intelligemment les macros au code, mais cela est mieux illustré ci-dessous:

Nous pouvons donc définir une macro appelée lcomp (abrégé pour la compréhension de liste). Sa syntaxe sera exactement comme celle de 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 en ligne de commande:

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

Plutôt chouette, 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 que vous pourriez souhaiter. Comme Python ou C # de with. La syntaxe LINQ de .NET. Au final, c'est ce qui attire les gens vers LISP - la flexibilité ultime.

288
gte525u

Vous trouverez un débat complet autour de 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 séparer la syntaxe du programme, l’analyser et la réassembler. Ils n'ont pas accès à l'analyseur du programme, ils doivent donc dépendre d'heuristiques et de suppositions. Parfois, leur analyse du taux de coupe est fausse, puis ils se cassent.

Mais LISP est différent. Les macros LISP do ont accès à l'analyseur. C'est un analyseur très simple. Une macro LISP ne reçoit pas une chaîne, mais un morceau de code source pré-évalué 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 bons pour séparer des listes et les reconstituer. Ils le font de manière fiable, tous les jours.

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" sur la valeur de l'expression "quel que soit".

LISP a aussi des listes; vous pouvez utiliser les fonctions "car" et "cdr" pour obtenir le premier élément d'une liste ou le reste de la liste, respectivement.

Maintenant que faire si vous souhaitez remplacer le premier élément d'une liste par une nouvelle valeur? Il existe une fonction standard pour le faire et, incroyablement, son nom est encore pire que "voiture". C'est "rplaca". Mais vous ne devez pas vous souvenir de "rplaca", car vous pouvez écrire

  (setf (car somelist) whatever)

mettre la voiture de somelist.

Ce qui se passe réellement 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 sur quelque chose. La fonction à utiliser pour cela est" rplaca "." Et cela réécrit discrètement le code en place pour:

  (rplaca somelist whatever)
102
VonC

Les macros LISP courantes étendent essentiellement les "primitives syntaxiques" de votre code.

Par exemple, en C, la construction switch/case ne fonctionne qu'avec les types intégraux et si vous souhaitez l’utiliser pour des flottants ou des chaînes, il vous reste des instructions if imbriquées 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 en entrée et retourne le code pour remplacer son "invocation", vous pouvez étendre votre répertoire de "primitives" aussi loin que vous le souhaitez, en terminant 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 s'y prendre, mais ce n'est pas nécessairement la plus facile.

53
Vatine

Les macros LISP vous permettent de décider quand (le cas échéant) une partie ou une expression sera évaluée. Pour donner un exemple simple, pensez aux C:

expr1 && expr2 && expr3 ...

Ceci dit: Evaluez expr1, et si cela devait être vrai, évaluer expr2, etc.

Maintenant, essayez de faire ceci && dans une fonction ... c'est vrai, vous ne pouvez pas. Appeler quelque chose comme:

and(expr1, expr2, expr3)

Va évaluer les trois exprs avant de donner une réponse, que expr1 était faux!

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 formulaires que vous lui transmettez sauf s’ils sont tous vrais.

Pour voir comment cela est utile, comparez:

(&& (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 vous permettent également de créer de nouveaux mots-clés et/ou mini-langues (consultez le (loop ...) macro par exemple), en intégrant d’autres langues dans LISP, par exemple, vous pouvez écrire une macro qui vous permet de dire quelque chose comme:

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

Et ce n'est même pas entrer dans Macros Reader .

J'espère que cela t'aides.

39
dsm

Je ne pense pas avoir jamais vu les macros LISP expliquées mieux que celles de ce type: http://www.defmacro.org/ramblings/LISP.html

29
Rayne

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 à votre guise. En fin de compte, la macro génère un autre fragment de programme, qui est exécuté à l'exécution.

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

Ceci pourrait être utilisé pour implémenter la syntaxe "sugar" comme le for each _ instruction using- clause, linq select- expressions et ainsi de suite, sous forme de macros qui se transforment en code sous-jacent.

Si Java avait des macros, vous pouvez implémenter la syntaxe Linq en Java, sans que Sun ait besoin de changer le langage de base.

Voici un pseudo-code indiquant comment une macro de style LISP en C # pour implémenter using pourrait ressembler:

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

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 la gestion de code répétitif, mais ils sont limités de manière très sévère.

  • La syntaxe macro/template limitée limite leur utilisation. Par exemple, vous ne pouvez pas écrire un modèle qui se développe à autre chose qu'une classe ou une fonction. Les macros et les modèles ne peuvent pas gérer facilement 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 dans LISP. Vous avez toute la puissance de LISP pour écrire la macro.
  • LISP a une syntaxe très régulière.

Parlez à toute personne maîtrisant le C++ et demandez-lui combien de temps elle a passé à apprendre tout le fudgery de modèles dont elle a besoin pour effectuer la métaprogrammation de modèles. Ou tous les trucs fous dans des livres (excellents) comme Modern C++ Design, qui sont encore difficiles à déboguer et (en pratique) non portables entre les compilateurs du monde réel, même si le langage a été normalisé pour un décennie. Tout cela fond si le langage que vous utilisez pour la métaprogrammation est le même langage que vous utilisez pour la programmation!

9
Matt Curtis

Je ne suis pas sûr de pouvoir ajouter un aperçu des (excellents) messages de tout le monde, mais ...

Les macros LISP fonctionnent très bien en raison de la nature syntaxique de LISP.

LISP est une langue extrêmement régulière (pensez à tout ce qui est une liste ) ; Les macros vous permettent de traiter les données et le code comme identiques (aucune analyse de chaîne ou autre piratage n'est nécessaire pour modifier les expressions LISP). Vous combinez ces deux fonctionnalités et vous disposez d'un moyen très propre pur de modifier le code.

Edit: Ce que j'essayais de dire, c'est que LISP est homoiconic , ce qui signifie que la structure de données d'un programme LISP est écrit en LISP lui-même.

Ainsi, vous vous retrouvez avec un moyen de créer votre propre générateur de code par dessus le langage, en utilisant le langage lui-même avec toute sa puissance (par exemple, dans Java vous devez vous frayer un chemin avec le bytecode woven Bien que certains frameworks tels que AspectJ vous permettent de le faire en utilisant une approche différente, c’est fondamentalement un hack).

En pratique, avec les macros, vous construisez votre propre mini-langage en plus du LISP, sans qu'il soit nécessaire d'apprendre d'autres langues ou outils, et d'utiliser toute la puissance du langage lui-même.

8
Miguel Ping

Étant donné que les réponses existantes fournissent de bons exemples concrets expliquant les résultats obtenus par les macros et comment, il serait peut-être utile de rassembler certaines des raisons pour lesquelles le mécanisme macro constitue un gain significatif par rapport à d'autres langues. ; D'abord de ces réponses, puis un grand venu d'ailleurs:

... en C, vous devez écrire un pré-processeur personnalisé [qui pourrait probablement être considéré comme un programme C suffisamment compliqué ] ...

Vatine

Parlez à toute personne maîtrisant le C++ et demandez-lui combien de temps elle a passé à apprendre tous les gabarits de gabarits dont elle a besoin pour faire de la métaprogrammation des gabarits [qui n'est toujours pas aussi puissant].

Matt Curtis

... in Java vous devez vous frayer un chemin avec le tissage de 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 à Perl foreach ou Python pour. Java a ajouté un type de construction de boucle similaire avec la boucle "améliorée" pour Java 1.5, dans le cadre de JSR-201. Remarquez quelle différence les macros font Un programmeur LISP qui remarque un motif commun dans son code peut écrire une macro pour se donner une abstraction au niveau source de ce motif. Un programmeur Java qui remarque que le même motif doit convaincre Sun que Cette abstraction mérite d’être ajoutée au langage. Ensuite, Sun doit publier un JSR et convoquer un "groupe d’experts" de l’industrie pour tout mettre en ordre. Ce processus - selon Sun - prend en moyenne 18 mois. , les rédacteurs du compilateur doivent tous mettre à niveau leurs compilateurs pour prendre en charge la nouvelle fonctionnalité. Et même une fois que le compilateur préféré du programmeur Java prend en charge la nouvelle version de Java, ils peuvent probablement le faire encore t utiliser la nouvelle fonctionnalité jusqu’à ce qu’ils soient autorisés à rompre la compatibilité de la source avec les anciennes versions de Java, ce qui est gênant pour les programmeurs Common LISP. dans les cinq minutes qui suivent la plaie Java programmeurs depuis des années.

Peter Seibel, dans "Practical Common LISP"

8
ZakW

Les macros LISP représentent un modèle qui se produit dans presque tous les projets de programmation importants. Finalement, dans un programme volumineux, vous avez une certaine section de code dans laquelle vous réalisez qu'il serait plus simple et moins sujet aux erreurs que d'écrire un programme qui affiche le code source sous forme de texte que vous pouvez simplement coller.

Dans Python ont deux méthodes __repr__ et __str__. __str__ est simplement la représentation lisible par l’homme. __repr__ renvoie une représentation valide Python code), 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ère un code valide qui peut être collé dans votre source.

Dans LISP, tout ce processus a été formalisé par le système macro. 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 entendu, il est utile que le système de macros LISP vous permette de manipuler ces "extraits" avec toute la puissance de la langue.

6
dnolen

En bref, les macros sont des transformations de code. Ils permettent d'introduire de nombreuses nouvelles constructions de syntaxe. Par exemple, considérons LINQ en C #. Dans LISP, des extensions de langage similaires sont implémentées par les macros (par exemple, construction de boucle intégrée, iterate). Les macros réduisent considérablement la duplication de code. Les macros permettent d’incorporer de "petits langages" (par exemple, dans le cas où c #/Java utiliserait XML pour configurer, dans LISP, la même chose peut être obtenue avec des macros). Les macros peuvent masquer les difficultés d'utilisation des bibliothèques.

Par exemple, dans 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 masque tout le contenu de la base de données (transactions, fermeture de la connexion, extraction des données, etc.), alors qu'en C #, cela nécessite la création de SqlConnections, SqlCommands, l'ajout de SqlParameters à SqlCommands, la boucle sur SqlDataReaders et leur fermeture correcte.

5
dmitry_vk

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 tous les paramètres avant d’appeler la fonction. Avec une macro, c'est l'inverse, LISP transmet les paramètres non évalués à la macro. Par exemple, si vous passez (+ 1 2) à une fonction, la fonction recevra la valeur 3. Si vous transmettez cette valeur à 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 boucle ou la déconstruction d'une liste
  • Mesurer le temps nécessaire pour exécuter une fonction transmise. Avec une fonction, le paramètre serait évalué avant que le contrôle ne soit passé à la fonction. Avec la macro, vous pouvez associer votre code entre le début et la fin de votre chronomètre. Le code ci-dessous contient exactement le même code dans une macro et une fonction et la sortie est très différente. Note: Ceci est un exemple artificiel et l'implémentation a été choisie pour qu'elle soit identique afin de 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
    
2
Joerg Schmuecker

Je l’ai tirée du livre de recettes LISP commun, mais je pense que cela explique pourquoi les macros LISP sont bonnes d’une manière agréable.

"Une macro est un morceau de code LISP ordinaire qui agit sur un autre morceau de code LISP putatif, le traduisant en (une version plus proche de) le LISP exécutable. Cela peut sembler un peu compliqué, alors donnons un exemple simple. Supposons que vous vouliez version de setq qui définit deux variables sur la même valeur, donc si vous écrivez

(setq2 x y (+ z 3))

quand z=8 x et y sont tous deux réglés sur 11. (Je ne vois aucune utilité à cela, mais ce n'est qu'un exemple.)

Il devrait être évident que nous ne pouvons pas définir setq2 en tant que fonction. Si x=50 et y=-5, cette fonction recevrait les valeurs 50, -5 et 11; il n'aurait aucune connaissance de quelles variables étaient supposées être définies. Ce que nous voulons vraiment dire, c’est: Quand vous (le système LISP) voyez (setq2 v1 v2 e), le traite comme équivalent à (progn (setq v1 e) (setq v2 e)). En fait, ce n’est pas tout à fait correct, mais cela ira pour le moment. Une macro nous permet de le faire précisément en spécifiant un programme pour transformer le motif d’entrée (setq2 v1 v2 e) "dans le motif de sortie (progn ...). "

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

0
nilsi