Évaluation paresseuse vs Macros

Je suis habitué à l'évaluation paresseuse de Haskell, et je suis irrité par les langues avides par défaut maintenant que j'ai utilisé l'évaluation paresseuse correctement. C'est en fait assez dommageable, car les autres langages que j'utilise principalement rendent l'évaluation paresseuse très maladroite, impliquant normalement le déploiement d'itérateurs personnalisés et ainsi de suite. Donc, juste en acquérant quelques connaissances, je me suis effectivement rendumoins productif dans mes langues d'origine. Soupir.

Mais J'entends que AST les macros offrent une autre façon propre de faire la même chose. J'ai souvent entendu des déclarations comme "L'évaluation paresseuse rend les macros redondantes" et vice-versa, principalement de sparring communautés Lisp et Haskell.

J'ai tâté avec des macros dans diverses variantes Lisp. Ils semblaient juste être un moyen vraiment organisé de copier et coller des morceaux de code à manipuler au moment de la compilation. Ils n'étaient certainement pas le Saint Graal que les Lispers aiment à penser qu'il est. Mais c'est presque certainement parce que je ne peux pas utiliser correctement. Bien sûr, le fait que le système de macro fonctionne sur la même structure de données de base avec laquelle le langage lui-même est assemblé est vraiment utile, mais c'est toujours un moyen organisé de copier-coller du code. Je reconnais que fonder un système de macro sur le même AST que le langage qui permet une modification complète de l'exécution est puissant.

Ce que je veux savoir, c'est comment les macros peuvent-elles être utilisées pour faire de manière concise et succincte ce que fait lazy-evaluation? Si je veux traiter une ligne de fichier par ligne sans siphonner le tout, je retourne juste une liste qui a eu une routine de lecture de ligne mappée dessus. C'est l'exemple parfait de DWIM (faites ce que je veux dire). Je n'ai même pas à y penser.

Je ne reçois clairement pas de macros. Je les ai utilisés et je n'ai pas été particulièrement impressionné compte tenu du battage médiatique. Il me manque donc quelque chose que je ne reçois pas en lisant la documentation en ligne. Quelqu'un peut-il expliquer tout cela à moi?

44
demandé sur acfoltzer 2011-08-13 02:24:12

5 réponses

L'évaluation paresseuse peut remplacer certaines utilisations de macros (celles qui retardent l'évaluation pour créer des constructions de contrôle) mais l'inverse n'est pas vraiment vrai. Vous pouvez utiliser des macros pour rendre les constructions d'évaluation retardée plus transparentes - voir la DDRS 41 (flux) pour un exemple de la façon suivante: http://download.plt-scheme.org/doc/4.1.5/html/srfi-std/srfi-41/srfi-41.html

En plus de cela, vous pouvez également écrire vos propres primitives io paresseuses.

Dans mon expérience, cependant, omniprésent le code paresseux dans un langage strict a tendance à introduire une surcharge par rapport au code paresseux omniprésent dans un runtime conçu pour le supporter efficacement dès le début-ce qui, rappelez-vous, est vraiment un problème d'implémentation.

21
répondu sclv 2011-08-12 22:55:21

L'évaluation paresseuse rend les macros redondantes

C'est un non-sens pur (pas de ta faute; je l'ai déjà entendu). Il est vrai que vous pouvez utiliser des macros pour changer l'ordre, le contexte, etc. de l'évaluation de l'expression, mais c'est l'utilisation la plus basique des macros, et il n'est vraiment pas pratique de simuler un langage paresseux en utilisant des macros ad-hoc au lieu de fonctions. Donc, si vous arriviez à des macros de cette direction, vous seriez en effet déçu.

Les Macros servent à étendre langue avec de nouvelles formes syntaxiques. Certaines des capacités spécifiques des macros sont

  1. affectant l'ordre, le contexte, etc. de l'évaluation de l'expression.
  2. créer de nouvelles formes de liaison (c'est-à-dire affecter la portée dans laquelle une expression est évaluée).
  3. effectuer des calculs à la compilation, y compris l'analyse et la transformation du code.

Les Macros qui font (1) peuvent être assez simples. Par exemple, dans raquette , le formulaire de gestion des exceptions, with-handlers, est juste une macro qui se développe en call-with-exception-handler, quelques conditions, et un code de continuation. Il est utilisé comme ceci:

(with-handlers ([(lambda (e) (exn:fail:network? e))
                 (lambda (e)
                   (printf "network seems to be broken\n")
                   (cleanup))])
  (do-some-network-stuff))

La macro implémente la notion de "clauses de prédicat et de gestionnaire dans le contexte dynamique de l'exception" basée sur la primitive call-with-exception-handler qui gère toutes les exceptions au moment où elles sont soulevées.

Une utilisation plus sophistiquée des macros est une implémentation d'un générateur D'analyseur LALR (1) . Au lieu d'un fichier séparé nécessitant un prétraitement, le parser la forme est juste un autre type d'expression. Il prend une description grammaticale, calcule les tables au moment de la compilation et produit une fonction d'analyseur. Les routines d'action sont lexicalement étendues, de sorte qu'elles peuvent se référer à d'autres définitions dans le fichier ou même à des variables liées à lambda. Vous pouvez même utiliser d'autres extensions de langue dans les routines d'action.

À l'extrémité, Typed raquette est un dialecte typé de raquette implémenté via des macros. Il a un système de type sophistiqué conçu pour correspondre les idiomes du code raquette / Scheme, et il interagit avec des modules non typés en protégeant les fonctions typées avec des contrats logiciels dynamiques (également implémentés via des macros). Il est implémenté par une macro "module typé" qui développe, vérifie et transforme le corps du module ainsi que des macros auxiliaires pour attacher des informations de type aux définitions, etc.

FWIW, il y a aussi Lazy raquette , un dialecte paresseux de raquette. Il n'est pas implémenté en transformant chaque fonction en macro, mais par rebinding lambda, define, et la syntaxe de l'application de fonction aux macros qui créent et forcent les promesses.

En résumé, l'évaluation paresseuse et les macros ont un petit point d'intersection, mais ce sont des choses extrêmement différentes. Et les macros ne sont certainement pas subsumées par une évaluation paresseuse.

58
répondu Ryan Culpepper 2016-03-03 09:23:32

La paresse est dénotatif, alors que les macros ne le sont pas. Plus précisément, si vous ajoutez une non-rigueur à un langage dénotatif, le résultat est toujours dénotatif, mais si vous ajoutez des macros, le résultat ne l'est pas. En d'autres termes, la signification d'une expression dans un langage pur paresseux est fonction uniquement des significations des expressions composantes; tandis que les macros peuvent produire des résultats sémantiquement distincts à partir d'arguments sémantiquement égaux.

En ce sens, les macros sont plus puissant, tandis que la paresse est en conséquence plus bien comportée sémantiquement.

Edit : plus précisément, les macros sont non dénotatives sauf {[3] } en ce qui concerne la dénotation identitaire/triviale (où la notion de "dénotatif" devient vide).

22
répondu Conal 2017-05-23 12:09:44

Lisp a commencé à la fin des années 50 du dernier millénaire. Voir fonctions récursives des EXPRESSIONS symboliques et leur calcul par MACHINE . Les Macros étaient Pas une partie de ce Lisp. L'idée était de calculer avec des expressions symboliques, qui peuvent représenter toutes sortes de formules et de programmes: expressions mathématiques, expressions logiques, phrases en langage naturel, programmes informatiques, ...

Plus tard, les macros Lisp ont été inventées et elles sont une application de cette idée ci-dessus pour Lisp lui-même: les Macros transforment des expressions Lisp (ou de type Lisp) en d'autres expressions Lisp en utilisant le langage Lisp complet comme langage de transformation.

Vous pouvez imaginer qu'avec les Macros, vous pouvez implémenter de puissants pré-processeurs et compilateurs en tant qu'utilisateur de Lisp.

Le dialecte Lisp typique utilise une évaluation stricte des arguments: tous les arguments des fonctions sont évalués avant qu'une fonction ne soit exécutée. Lisp a également plusieurs formulaires intégrés qui ont des règles d'évaluation différentes. IF est un tel exemple. En Common Lisp IF est un soi-disant opérateur spécial.

Mais nous pouvons définir un nouveau langage (sous -) de type Lisp qui utilise une évaluation paresseuse et nous pouvons écrire des Macros pour transformer ce langage en Lisp. Ceci est une application pour les macros, mais de loin pas la seule.

Un exemple (relativement ancien) d'une telle extension Lisp qui utilise des macros pour implémenter un transformateur de code qui fournit des structures de données avec une évaluation paresseuse est l'extension SERIES à Common Lisp.

9
répondu Rainer Joswig 2011-08-13 18:52:20

Les Macros peuvent être utilisées pour gérer l'évaluation paresseuse, mais n'en font qu'une partie. Le point principal des macros est que grâce à eux, rien n'est fixé dans la langue.

Si la programmation est comme jouer avec des briques LEGO, avec des macros, vous pouvez également changer la forme des briques ou le matériau avec lequel elles sont construites.

Macros est plus qu'une simple évaluation retardée. Cela était disponible en tant que fexpr (un précurseur de macro dans l'histoire de lisp). Macros est sur la réécriture du programme, où fexpr est juste un cas particulier...

A titre d'exemple, considérez que j'écris dans mon temps libre un petit lisp au compilateur javascript et à l'origine (dans le noyau javascript) je n'avais que lambda avec le support des arguments &rest. Maintenant, il y a un support pour les arguments de mots clés et cela parce que j'ai redéfini ce que lambda signifie dans lisp lui-même.

Je peux maintenant écrire:

(defun foo (x y &key (z 12) w) ...)

Et appelez la fonction avec

(foo 12 34 :w 56)

Lors de l'exécution de cet appel, dans le corps de la fonction, le paramètre w sera lié à 56 et le paramètre z à 12 car il n'a pas été passé. Je vais également obtenir une erreur d'exécution si un argument de mot-clé non pris en charge est passé à la fonction. Je pourrais même ajouter un support de vérification à la compilation en redéfinissant ce que signifie compiler une expression (c'est-à-dire en ajoutant des vérifications si les formulaires d'appel de fonction "statiques" transmettent les paramètres corrects aux fonctions).

Le point central est que le langage original (noyau) n'avait pas du tout de support pour les arguments de mots clés, et j'ai pu l'ajouter à l'aide de la langue elle-même. Le résultat est exactement comme s'il était là depuis le début; il fait simplement partie de la langue.

La syntaxe est importante (même s'il est techniquement possible d'utiliser une machine turing). La syntaxe façonne les pensées que vous avez. Les Macros (et les macros lues) vous donnent un contrôle total sur la syntaxe.

Un point clé est que le code de réécriture de code n'utilise pas un langage de type brainf**K paralysé comme métaprogrammation de modèle C++ (où faire un if est un réalisation étonnante), ou avec un moteur de substitution moins que regexp encore plus stupide comme le préprocesseur C.

Code-le code de réécriture utilise le même langage complet (et extensible). C'est lisp tout le long; -)

Bien sûr, écrire des macros est plus difficile que d'écrire du code normal; mais c'est une "complexité essentielle" du problème, pas une complexité artificielle parce que vous êtes obligé d'utiliser un demi-langage muet comme avec la métaprogrammation C++.

L'écriture de macros est plus difficile car le code est une chose complexe et lorsque vous écrivez des macros, vous écrivez des choses complexes qui construisent elles-mêmes des choses complexes. Il n'est même pas si rare de monter d'un niveau de plus et d'écrire des macros générant des macros (c'est de là que vient la vieille blague de lisp de "j'écris du code qui écrit du code qui écrit du code pour lequel je suis payé").

Mais le pouvoir macro est tout simplement illimité.

5
répondu 6502 2014-12-29 14:10:48