Evaluation paresseuse vs Macros

Je suis habitué à l’évaluation paresseuse de Haskell, et je me trouve irrité par les langues par défaut maintenant que j’ai utilisé l’évaluation paresseuse correctement. Ceci est en fait très dommageable, car les autres langages que j’utilise font surtout des évaluations paresseuses très compliquées, impliquant normalement le déploiement d’iterators personnalisés, etc. Donc, en acquérant des connaissances, je me suis rendu moins productif dans mes langues d’origine. Soupir.

Mais j’entends que les macros AST 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 superflues» et vice versa, principalement à partir des communautés de Lisp et Haskell.

J’ai touché aux macros dans différentes variantes de Lisp. Ils semblaient juste être une manière vraiment organisée de copier et de coller des morceaux de code pour être traités au moment de la compilation. Ils n’étaient certainement pas le Saint Graal que Lispers aime à penser. Mais c’est presque certainement parce que je ne peux pas les utiliser correctement. Bien sûr, le fait de faire fonctionner le macro-système sur la même structure de données de base que le langage lui-même est assemblé est vraiment utile, mais il s’agit toujours d’une manière organisée de copier-coller du code. Je reconnais que le fait de baser un macro-système sur le même AST que le langage autorisant 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 l’évaluation paresseuse? Si je veux traiter un fichier ligne par ligne sans encombrer le tout, je renvoie simplement une liste sur laquelle une routine de lecture de ligne a été mappée. C’est l’exemple parfait de DWIM (fais ce que je veux dire). Je n’ai même pas besoin d’y penser.

Je n’ai clairement pas de macros. Je les ai utilisés et je n’ai pas été particulièrement impressionné par le battage médiatique. Donc, il me manque quelque chose que je ne lis pas en lisant la documentation en ligne. Est-ce que quelqu’un peut m’expliquer tout cela?

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ées plus transparentes – voir SRFI 41 (Streams) pour un exemple de comment: 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, le code paresseux dans un langage ssortingct a tendance à introduire une surcharge par rapport au code paresseux dans un environnement d’exécution conçu pour le prendre en charge dès le départ, ce qui constitue un problème d’implémentation.

L’évaluation paresseuse rend les macros superflues

C’est un pur non-sens (pas de ta faute, je l’ai déjà entendu). Il est vrai que vous pouvez utiliser des macros pour modifier l’ordre, le contexte, etc. de l’évaluation des expressions, mais c’est l’utilisation la plus élémentaire des macros, et il n’est pas pratique de simuler un langage paresseux en utilisant des macros ad hoc. Donc, si vous êtes venu dans les macros de cette direction, vous seriez en effet déçu.

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

  1. Affectant l’ordre, le contexte, etc. de l’évaluation des expressions.
  2. Créer de nouvelles formes de liaison (c.-à-d. Affecter la scope dans laquelle une expression est évaluée).
  3. Effectuer des calculs à la compilation, y compris l’parsing du code et la transformation.

Les macros (1) peuvent être assez simples. Par exemple, dans Racket , le formulaire de gestion des exceptions, with-handlers , est simplement une macro qui se développe en call-with-exception-handler , certaines 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 dynamic de l’exception” basée sur le primitif call-with-exception-handler qui gère toutes les exceptions au point où elles sont déclenchées.

Une utilisation plus sophistiquée des macros est une implémentation d’un générateur d’parsingur LALR (1) . Au lieu d’un fichier séparé nécessitant un prétraitement, le formulaire d’ parser est un autre type d’expression. Il prend une description grammaticale, calcule les tables au moment de la compilation et produit une fonction d’parsing. Les routines d’action ont une scope lexicale et peuvent donc se référer à d’autres définitions du fichier ou même aux variables lambda . Vous pouvez même utiliser d’autres extensions de langue dans les routines d’action.

À l’extrême, Typed Racket est un dialecte typé de Racket implémenté via des macros. Il possède un système de type sophistiqué conçu pour correspondre aux idiomes du code Racket / Scheme, et interagit avec les modules non typés en protégeant les fonctions typées des contrats logiciels dynamics (également implémentés via des macros). Il est implémenté par une macro “module typé” qui développe, contrôle les caractères et transforme le corps du module ainsi que les macros auxiliaires pour attacher des informations de type aux définitions, etc.

FWIW, il y a aussi Lazy Racket , un dialecte paresseux de Racket. Il n’est pas implémenté en transformant chaque fonction en macro, mais en reliant lambda , define et la syntaxe d’application de la 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 englobées par une évaluation paresseuse.

La paresse est dénotative , 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 n’est pas dénotatif. En d’autres termes, la signification d’une expression dans un langage pur paresseux est uniquement fonction des significations des expressions composant; tandis que les macros peuvent donner des résultats sémantiquement distincts à partir d’arguments sémantiquement égaux.

En ce sens, les macros sont plus puissantes, tandis que la paresse est sémantiquement plus sage.

Edit : plus précisément, les macros ne sont pas dénotatives, sauf en ce qui concerne la dénotation identitaire / sortingviale (où la notion de “dénotatif” devient vide).

Lisp a commencé à la fin des années 50 du dernier millénaire. Voir FONCTIONS DE RÉCURSION DES EXPRESSIONS SYMBOLIQUES ET LEUR CALCUL PAR MACHINE . Les macros ne faisaient pas 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, …

Les macros Lisp ultérieures ont été inventées et elles constituent une application de cette idée à Lisp elle-même: les macros transforment les expressions Lisp (ou Lisp) en d’autres expressions Lisp utilisant le langage Lisp complet comme langage de transformation.

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

Le dialecte Lisp typique utilise une évaluation ssortingcte des arguments: tous les arguments des fonctions sont évalués avant l’exécution d’une fonction. Lisp dispose également de plusieurs formulaires intégrés qui ont des règles d’évaluation différentes. IF c’est un exemple. En Common Lisp IF est un opérateur dit spécial .

Mais nous pouvons définir un nouveau (sous) langage de type Lisp qui utilise l’é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) pour 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 différée est l’extension SERIES à Common Lisp.

Les macros peuvent être utilisées pour gérer une évaluation paresseuse, mais en sont juste une partie. Le point principal des macros est que grâce à elles, rien n’est corrigé dans le langage.

Si la programmation est comme jouer avec des briques LEGO, avec les macros, vous pouvez également modifier la forme des briques ou du matériel avec lequel elles sont construites.

Les macros sont plus que des évaluations différées. C’était disponible comme fexpr (un précurseur de macro dans l’histoire de lisp). Les macros concernent la réécriture de programmes, où fexpr n’est qu’un cas particulier …

À titre d’exemple, je pense que j’écris dans mon temps libre un minuscule lisp pour le compilateur javascript et, à l’origine (dans le kernel javascript), je n’avais que des arguments lambda avec prise en charge &rest . Maintenant, il y a un support pour les arguments de mots-clés et cela parce que j’ai redéfini ce que signifie lambda dans lisp lui-même.

Je peux maintenant écrire:

 (defun foo (xy &key (z 12) w) ...) 

et appeler 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é transmis. J’obtiendrai également une erreur d’exécution si un argument de mot clé non pris en charge est transmis à la fonction. Je pourrais même append un support de vérification à la compilation en redéfinissant ce que signifie la compilation d’une expression (c’est-à-dire en ajoutant des contrôles si les formulaires d’appel de fonction “statique” transmettent les parameters corrects aux fonctions).

Le point central est que le langage d’origine (kernel) ne supportait pas du tout les arguments de mots clés, et j’ai pu l’append en utilisant le langage lui-même. Le résultat est exactement comme s’il était là depuis le début; c’est simplement une partie de la langue.

La syntaxe est importante (même s’il est techniquement possible d’utiliser une machine de turing). La syntaxe façonne les pensées que vous avez. Les macros (et les macros de lecture) vous permettent de contrôler totalement la syntaxe.

Un point clé est que le code de réécriture de code n’utilise pas un langage déformé et déformé tel que la métaprogrammation de modèles C ++ (où faire un if est une réussite incroyable), ou avec une expression moins rationnelle. moteur de substitution comme préprocesseur C.

Le code de réécriture de code utilise le même langage complet (et extensible). C’est lisp tout en bas 😉

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, car vous êtes obligé d’utiliser un demi-langage idiot 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 créent des choses complexes elles-mêmes. Il n’est même pas rare de remonter d’un niveau et d’écrire des macros générasortingces de macros (c’est de là que provient l’ancienne blague «J’écris du code qui écrit du code qui écrit le code pour lequel je suis payé»).

Mais le macro-pouvoir est tout simplement sans limite.