J'ai entendu dire que Lisp vous permet de redéfinir le langage lui-même, et j'ai essayé de faire des recherches à ce sujet, mais il n'y a aucune explication claire nulle part. Quelqu'un a-t-il un exemple simple ?
Réponses
Trop de publicités?Les utilisateurs de Lisp se réfèrent à Lisp comme à la langage de programmation programmable . Il est utilisé pour calcul symbolique - le calcul avec des symboles.
Les macros ne sont qu'un moyen d'exploiter le paradigme du calcul symbolique. La vision plus large est que Lisp fournit des moyens faciles de décrire des expressions symboliques : termes mathématiques, expressions logiques, instructions d'itération, règles, descriptions de contraintes et plus encore. Les macros (transformations de formes sources Lisp) ne sont qu'une application du calcul symbolique.
Il y a certains aspects à cela : Si vous demandez de "redéfinir" la langue, alors redéfinir strictement signifierait redéfinir un mécanisme linguistique existant (syntaxe, sémantique, pragmatique). Mais il y a aussi l'extension, l'intégration et la suppression des caractéristiques du langage.
Dans la tradition Lisp, il y a eu de nombreuses tentatives pour fournir ces fonctionnalités. Un dialecte Lisp et une certaine implémentation peuvent n'en offrir qu'un sous-ensemble.
Quelques façons de redéfinir/changer/étendre les fonctionnalités fournies par les principales implémentations de Common Lisp :
-
syntaxe de l'expression s . La syntaxe des expressions s n'est pas fixée. Le lecteur (la fonction READ) utilise ce qu'on appelle lire les tableaux pour spécifier les fonctions qui seront exécutées lorsqu'un caractère est lu. On peut modifier et créer des tables de lecture. Cela permet par exemple de modifier la syntaxe de listes, de symboles ou d'autres objets de données. On peut également introduire une nouvelle syntaxe pour des types de données nouveaux ou existants (comme les tables de hachage). Il est également possible de remplacer complètement la syntaxe des expressions s et d'utiliser un mécanisme d'analyse syntaxique différent. Si le nouvel analyseur renvoie des formes Lisp, aucun changement n'est nécessaire pour l'interpréteur ou le compilateur. Un exemple typique est une macro de lecture qui peut lire des expressions infixes. Dans une telle macro de lecture, des expressions infixes et des règles de précédence pour les opérateurs sont utilisées. Les macros de lecture sont différentes des macros ordinaires : les macros de lecture travaillent au niveau des caractères de la syntaxe des données Lisp.
-
le remplacement des fonctions . Les fonctions de haut niveau sont liées à des symboles. L'utilisateur peut modifier cette liaison. La plupart des implémentations disposent d'un mécanisme permettant de le faire, même pour de nombreuses fonctions intégrées. Si vous souhaitez fournir une alternative à la fonction intégrée ROOM, vous pouvez remplacer sa définition. Certaines implémentations affichent une erreur et offrent ensuite la possibilité de poursuivre la modification. Il est parfois nécessaire de déverrouiller un paquet. Cela signifie que les fonctions en général peuvent être remplacées par de nouvelles définitions. Il y a des limites à cela. L'une d'entre elles est que le compilateur peut mettre les fonctions en ligne dans le code. Pour voir un effet, il faut alors recompiler le code qui utilise le code modifié.
-
fonctions de conseil . Souvent, on veut ajouter un certain comportement aux fonctions. Cela s'appelle "conseiller" dans le monde Lisp. De nombreuses implémentations de Common Lisp offrent une telle possibilité.
-
forfaits personnalisés . Les paquets regroupent les symboles dans des espaces de noms. Le paquet COMMON-LISP abrite tous les symboles qui font partie de la norme ANSI Common Lisp. Le programmeur peut créer de nouveaux paquets et importer des symboles existants. Ainsi, vous pouvez utiliser dans vos programmes un paquetage EXTENDED-COMMON-LISP qui offre des possibilités supplémentaires ou différentes. En ajoutant simplement (IN-PACKAGE "EXTENDED-COMMON-LISP"), vous pouvez commencer à développer en utilisant votre propre version étendue de Common Lisp. Selon l'espace de noms utilisé, le dialecte Lisp que vous utilisez peut être légèrement ou même radicalement différent. Dans Genera on the Lisp Machine, plusieurs dialectes Lisp sont ainsi côte à côte : ZetaLisp, CLtL1, ANSI Common Lisp et Symbolics Common Lisp.
-
CLOS et des objets dynamiques. Le Common Lisp Object System intègre le changement. Le protocole de méta-objet étend ces capacités. CLOS lui-même peut être étendu/redéfini dans CLOS. Vous voulez un héritage différent. Ecrivez une méthode. Vous voulez différentes façons de stocker les instances. Ecrivez une méthode. Les slots devraient avoir plus d'informations. Fournissez une classe pour cela. CLOS lui-même est conçu de telle sorte qu'il est capable de mettre en œuvre toute une "région" de différents langages de programmation orientés objet. Des exemples typiques sont l'ajout de choses comme les prototypes, l'intégration avec des systèmes d'objets étrangers (comme Objective C), l'ajout de la persistance, ...
-
Formes de Lisp . L'interprétation des formes Lisp peut être redéfinie avec des macros. Une macro peut analyser le code source qu'elle englobe et le modifier. Il existe plusieurs façons de contrôler le processus de transformation. Les macros complexes utilisent un marcheur de code, qui comprend la syntaxe des formes Lisp et peut appliquer des transformations. Les macros peuvent être triviales, mais peuvent aussi devenir très complexes comme les macros LOOP ou ITERATE. D'autres exemples typiques sont les macros pour la génération de SQL embarqué et de HTML embarqué. Les macros peuvent également être utilisées pour déplacer le calcul au moment de la compilation. Comme le compilateur est lui-même un programme Lisp, des calculs arbitraires peuvent être effectués pendant la compilation. Par exemple, une macro Lisp peut calculer une version optimisée d'une formule si certains paramètres sont connus pendant la compilation.
-
Symboles . Common Lisp fournit des macros de symboles. Les macros de symboles permettent de modifier la signification des symboles dans le code source. Un exemple typique est le suivant : (with-slots (foo) bar (+ foo 17)) Ici, le symbole FOO dans le source entouré de WITH-SLOTS sera remplacé par un appel (slot-value bar 'foo).
-
optimisations Avec les macros de compilation, on peut fournir des versions plus efficaces de certaines fonctionnalités. Le compilateur utilisera ces macros de compilateur. C'est un moyen efficace pour l'utilisateur de programmer des optimisations.
-
Traitement de l'état - gérer les conditions qui résultent de l'utilisation du langage de programmation d'une certaine manière. Common Lisp fournit une manière avancée de gérer les erreurs. Le système de conditions peut également être utilisé pour redéfinir les caractéristiques du langage. Par exemple, on peut gérer les erreurs de fonctions indéfinies avec un mécanisme de chargement automatique écrit par l'utilisateur. Au lieu d'atterrir dans le débogueur lorsqu'une fonction indéfinie est vue par Lisp, le gestionnaire d'erreurs pourrait essayer d'autoloader la fonction et réessayer l'opération après avoir chargé le code nécessaire.
-
Variables spéciales - injecter des liaisons de variables dans du code existant. De nombreux dialectes Lisp, comme Common Lisp, fournissent des variables spéciales/dynamiques. Leur valeur est recherchée au moment de l'exécution sur la pile. Cela permet au code environnant d'ajouter des liaisons de variables qui influencent le code existant sans le modifier. Un exemple typique est une variable comme *standard-output*. On peut relier la variable et toute sortie utilisant cette variable pendant la portée dynamique de la nouvelle liaison ira dans une nouvelle direction. Richard Stallman a fait valoir que cela était très important pour lui et que cela a été rendu par défaut dans Emacs Lisp (même si Stallman connaissait la liaison lexicale dans Scheme et Common Lisp).
Lisp possède ces facilités et bien d'autres encore, car il a été utilisé pour implémenter de nombreux langages et paradigmes de programmation différents. Un exemple typique est l'implémentation intégrée d'un langage logique, par exemple Prolog. Lisp permet de décrire les termes Prolog avec des expressions s et avec un compilateur spécial, les termes Prolog peuvent être compilés en code Lisp. Parfois, la syntaxe Prolog habituelle est nécessaire, alors un analyseur syntaxique parse les termes Prolog typiques en formes Lisp, qui seront ensuite compilées. D'autres exemples de langages intégrés sont les langages à base de règles, les expressions mathématiques, les termes SQL, l'assembleur Lisp en ligne, HTML, XML et bien d'autres encore.
Je vais préciser que Scheme est différent de Common Lisp lorsqu'il s'agit de définir une nouvelle syntaxe. Il vous permet de définir des modèles en utilisant define-syntax
qui sont appliquées à votre code source partout où elles sont utilisées. Elles ressemblent à des fonctions, mais elles s'exécutent au moment de la compilation et transforment l'AST.
Voici un exemple de la façon dont let
peut être défini en termes de lambda
. La ligne avec let
est le motif à faire correspondre, et la ligne avec lambda
est le modèle de code résultant.
(define-syntax let
(syntax-rules ()
[(let ([var expr] ...) body1 body2 ...)
((lambda (var ...) body1 body2 ...) expr ...)]))
Notez que cela n'a RIEN à voir avec la substitution textuelle. Vous pouvez en fait redéfinir lambda
et la définition ci-dessus pour let
fonctionnera toujours, parce qu'il utilise la définition de lambda
dans l'environnement où let
a été défini. En gros, c'est puissant comme les macros mais propre comme les fonctions.
Les macros sont la raison habituelle pour laquelle on dit cela. L'idée est que, puisque le code n'est qu'une structure de données (un arbre, plus ou moins), vous pouvez écrire des programmes pour générer cette structure de données. Tout ce que vous savez sur l'écriture de programmes qui génèrent et manipulent des structures de données s'ajoute donc à votre capacité à coder de manière expressive.
Les macros ne sont pas tout à fait une redéfinition complète du langage, du moins pour autant que je sache (je suis en fait un Schemer ; je peux me tromper), car il y a une restriction. Une macro ne peut prendre qu'un seul sous-arbre de votre code, et générer un seul sous-arbre pour le remplacer. Par conséquent, vous ne pouvez pas écrire des macros transformant des programmes entiers, aussi cool que cela puisse être.
Cependant, les macros, telles qu'elles sont, peuvent encore faire beaucoup de choses - certainement plus que ce que n'importe quel autre langage vous permet de faire. Et si vous utilisez la compilation statique, il ne serait pas difficile du tout de faire une transformation de tout le programme, donc la restriction est moins importante.
Cette réponse concerne spécifiquement le Common Lisp (CL ci-après), bien que certaines parties de la réponse puissent être applicables à d'autres langages de la famille lisp.
Comme CL utilise des expressions S et ressemble (principalement) à une séquence d'applications de fonctions, il n'y a pas de différence évidente entre les modules intégrés et le code utilisateur. La principale différence est que les "choses que le langage fournit" sont disponibles dans un paquetage spécifique au sein de l'environnement de codage.
Avec un peu d'attention, il n'est pas difficile de coder des remplacements et de les utiliser à la place.
Maintenant, le lecteur "normal" (la partie qui lit le code source et le transforme en notation interne) s'attend à ce que le code source soit dans un format assez spécifique (expressions S entre parenthèses) mais comme le lecteur est piloté par quelque chose appelé "tables de lecture" et que celles-ci peuvent être créées et modifiées par le développeur, il est également possible de changer l'apparence du code source.
Ces deux éléments devraient au moins permettre de justifier pourquoi Common Lisp peut être considéré comme un langage de programmation reprogrammable. Je n'ai pas d'exemple simple sous la main, mais j'ai une implémentation partielle d'une traduction de Common Lisp en suédois (créée pour le 1er avril, il y a quelques années).
De l'extérieur, en regardant à l'intérieur...
J'ai toujours pensé que c'était parce que Lisp fournissait, à la base, des opérateurs logiques si basiques et atomiques que n'importe quel processus logique peut être construit (et a été construit et fourni sous forme d'outils et de compléments) à partir des composants de base.
Ce n'est pas tant qu'elle puisse se redéfinir que sa définition de base soit si malléable qu'elle puisse prendre n'importe quelle forme et qu'aucune forme ne soit assumée/présumée dans la structure.
Par métaphore, si vous n'avez que des composés organiques, vous faites de la chimie organique, si vous n'avez que des oxydes métalliques, vous faites de la métallurgie, mais si vous n'avez que des éléments, vous pouvez tout faire, mais vous avez des étapes initiales supplémentaires à accomplir.... dont la plupart ont déjà été faites par d'autres.....
Je pense que.....