Le langage Python homoiconique
(aljamal.substack.com)- L’homoiconicité de Lisp est la propriété qui consiste à traiter le code et les données sous la même forme ; transposer le classique « Lisp in Lisp » en Python fait ressortir cette idée dans une syntaxe plus familière
- À l’origine, Lisp faisait coexister les M-expressions, représentation du code, et les S-expressions, représentation des données, et « Lisp in Lisp » implémentait un Lisp à S-expressions au moyen de M-expressions
- La version Python représente les S-expressions par des listes Python et transpose les M-expressions en appels de fonctions et en conditions, ce qui permet de construire un interpréteur sans parseur séparé
- Le premier interpréteur repose sur des opérations primitives de liste comme
atom,eq,car,cdr,consetappend; pour prendre en chargelambda,assoc,pairliset une liste d’environnement sont ajoutés evalreçoit à la fois l’expression et l’environnement pour gérer les liaisons de variables, et associe arguments et valeurs selon un scope dynamique viapairlisetassoc
L’unification du code et des données montrée par Lisp
- Lisp a laissé plusieurs idées encore pertinentes des décennies plus tard, à travers le papier sur Lisp de John McCarthy au début des années 1960 et le manuel Lisp 1.5
- L’une des idées centrales est l’homoiconicité
- Dans un langage ordinaire, le code est compris comme une suite d’opérations agissant sur des données
- Lisp traite le code et les données sous la même forme, brouillant la frontière entre opérateurs et opérandes
- Alan Kay a qualifié le code « Lisp in Lisp » en bas de la page 13 du manuel Lisp 1.5 d’« équations de Maxwell du logiciel »
- La citation incluse souligne qu’il y voyait une grande révélation : quelques lignes de code contenaient tout l’univers de la programmation
Comment transposer « Lisp in Lisp » en Python
- L’objectif est de réécrire en Python le classique « Lisp in Lisp » tout en conservant autant que possible l’esprit du code d’origine
- Lisp possède deux représentations syntaxiques
- M-expression : représentation du code, abréviation de meta
- S-expression : représentation des données, abréviation de symbolic
- Les deux représentations sont sémantiquement équivalentes
- Le code « Lisp in Lisp » existant est écrit en M-expressions et implémente un Lisp à S-expressions
- Dans l’implémentation Python, les S-expressions de Lisp sont représentées par des listes Python
- Lisp est l’abréviation de « List Processing » et fonctionne autour d’une structure de données unique : la liste
- Les listes Python sont utilisées comme une représentation adaptée pour émuler les S-expressions de Lisp
- Les M-expressions sont traduites en structures de code Python comme les appels de fonctions et les conditions
- Grâce à ce mapping, il est possible de créer un interpréteur sans manipulation de chaînes ni implémentation d’un parseur séparé
Un premier interpréteur construit avec des primitives de liste
- Une implémentation de Lisp nécessite quelques fonctions de base fournies depuis l’extérieur du langage
- Les primitives de liste utilisées dans l’implémentation Python sont les suivantes
atom(x): vérifie sixest une listeeq(x,y): vérifie sixetysont égauxcar(x): premier élément d’une listecdr(x): reste de la listecons(x,y): ajoute un atome à une listeappend(x,y): concatène deux listes
- À l’exception de quelques primitives récursives, il a été possible d’utiliser Llama3-70b sur Groq pour créer rapidement un interpréteur exécutant un sous-ensemble du code « Lisp in Lisp »
- Dans les exemples, les listes Python se comportent comme des S-expressions
- Le code complet est disponible dans des gists GitHub
Extensions pour lambda et la récursion
- La première implémentation omet une fonctionnalité importante :
lambdalambdaest le principal mécanisme de Lisp pour définir et appeler des fonctions anonymes- Sans
lambda, il est impossible d’implémenter la récursion en Lisp - Sans récursion, on n’atteint pas le critère minimal permettant de calculer tout ce qui est calculable : la complétude de Turing
- Pour ajouter
lambda,assoc(x,y)etpairlis(x,y)sont introduitsassoc(x,y)est une recherche clé/valeur implémentée avec des listes et utilise une liste d’associationspairlis(x,y)associe deux listes, commezip(x,y)en Python
- Dans Lisp d’origine, même un simple scan linéaire devait être traité par récursion
- C’était dû au fait que Lisp ne disposait pas de boucles à l’origine
- Dans la traduction Python,
assocetpairlispeuvent être exprimés plus succinctement avec des compréhensions de liste
- Pour le traitement de
COND, leevcondu Lisp d’origine est traduit en boucle, et la même approche est appliquée àevlisdans le traitement deLAMBDA
Liste d’environnement et scope dynamique
- La fonction
evaldu Lisp d’origine prend deux arguments- Le premier est la S-expression à évaluer
- Le second est une liste d’environnement composée de paires clé/valeur
- L’environnement conserve les liaisons de variables dans le traitement de
LAMBDA- Si une fonction possède une variable
xet qu’on lui affecte des données,pairlisassocie le symbolexaux données - La valeur associée est stockée dans la liste d’environnement ou y est ajoutée
- Quand
xest nécessaire,assocle recherche dans l’environnement et le réinjecte dans l’expression
- Si une fonction possède une variable
- Ce mode de liaison est appelé scope dynamique
- L’implémentation finale est une transposition en Python du « Lisp in Lisp » d’origine, et le dernier exemple inclut même l’exécution de
lambda
2 commentaires
Avis sur Hacker News
Si vous vous intéressez à un Lisp de scripting simple et rapide dans l’esprit de Python, les langages de style Clojure Hy et Janet valent le détour
Il est fourni sous forme de binaire à démarrage rapide et inclut aussi des bibliothèques pour les tâches du quotidien
Pour la partie CL↔Python, https://github.com/CodyReichert/awesome-cl?tab=readme-ov-fil... mérite aussi d’être consulté
Je suis mainteneur de Hy, et si vous voyez un écart de performances significatif, c’est un bug
Les M-expressions évitent ce piège ; je me demande donc s’il a existé un vrai langage qui conserve l’homoiconicité et l’élégance conceptuelle de Lisp sans imposer ces parenthèses
C’est exactement ce à quoi vous pensez. La description est elle aussi très directe : « Programmer en YAML »
Il n’y a presque rien à ajouter
Attendez, ce n’est pas simplement un Lisp implémenté en Python ?
Ça ne ressemble pas à du Python homoiconique, comme le titre le suggère ; qu’est-ce que j’ai raté ?
Pour cela, il faudrait que la syntaxe de Python soit composée de tuples, de listes et de dictionnaires, et que l’interpréteur les évalue directement
Un autre langage fonctionnel qui peut être implémenté de façon concise en Python est le Binary Lambda Calculus
Une grande partie du code traite le modèle d’entrée-sortie pur du BLC, et, pour la recherche de variables, il indexe un tableau d’environnement avec des indices de de Bruijn au lieu d’utiliser une liste d’associations
La même page propose aussi des implémentations dans 9 autres langages, et l’auto-interpréteur du BLC est le plus concis avec 232 bits (29 octets), parseur et tokenizer inclus
[1] https://rosettacode.org/wiki/Universal_Lambda_Machine#Python
Le cours d’introduction à la programmation du MIT fait écrire à tous les étudiants un interpréteur Lisp en Python
C’est une trace de l’époque où ce cours était effectivement donné en Lisp
https://py.mit.edu/spring24
J’ai fait quelque chose de similaire avec des listes JS : https://github.com/andrelaszlo/js-lisp
Il est à la fois drôle et ironique de voir les langages modernes redécouvrir, des décennies plus tard, les fonctionnalités étonnantes de Lisp
Il y a quelques jours, un programme Python qui appelait l’API de mon serveur domestique depuis 9 heures s’est arrêté, ce qui m’a mis en colère. Il s’agissait d’appels d’API similaires, et le programme appelait un LLM avec des modèles de prompts prédéfinis et des contraintes de grammaire
Avant de relancer séparément les itérations restantes, je voulais sauvegarder l’état du programme et l’arrêter, mais je n’ai trouvé aucun moyen de modifier le code Python en cours d’exécution ni d’inspecter les variables, et j’ai fini par perdre 9 heures de travail
Quelques jours plus tard, je suis tombé sur https://malisper.me/debugging-lisp-part-1-recompilation/, et j’ai été surpris de découvrir que Common Lisp intégrait ce genre de fonctionnalité au langage depuis des décennies. Il en va de même pour d’autres fonctionnalités, comme le système de macros le plus puissant
Mais c’est aussi le problème. Ils sont trop expressifs
Lisp m’a toujours fait penser aux arts visuels en techniques mixtes : la liberté d’expression paraît séduisante, mais au final les résultats sont souvent globalement inférieurs à ceux d’arts plus traditionnels à médium unique. On constate que les contraintes du médium sont aussi importantes que l’expressivité
Il y a aussi https://github.com/malor/cpython-lldb
Pour d’autres méthodes : https://github.com/albertz/pydbattach/
En quoi cette implémentation est-elle du Python homoiconique ?
Existe-t-il un Lisp doté d’un système de types qui aide à contrôler, dans une certaine mesure, la tendance de Lisp à devenir difficile à lire à mesure que les programmes grossissent ?
Même si ce n’est pas un système de types, je me demande s’il existe d’autres éléments capables de maîtriser cette propriété
La métaprogrammation est vraiment géniale, mais parfois cela se lit comme les abstractions Haskell les plus obscures que j’aie vues, sans même les signatures de types pour servir de guide
Je considère les systèmes de types et les linters comme les meilleurs outils pour discipliner automatiquement le code, mais j’ai du mal à imaginer comment contenir les tendances des projets Lisp sans restreindre précisément ce pour quoi on choisit Lisp
SBCL, qui est de fait ce qui se rapproche le plus d’un standard, offre aussi une assez bonne vérification des types à la compilation
Par exemple, si l’on écrit
(+ a b)après(declare (type String a b)), il détecte l’avertissement indiquant que le type dérivé deAestSTRINGalors qu’il devrait êtreNUMBERCette vérification de types a été la principale raison pour laquelle je suis passé de Scheme à CL, et je suis ensuite resté pour de petites fonctionnalités appréciables comme les restarts et les continuable asserts
Un Python orienté expressions serait bien meilleur que le Python actuel
Cet article ne traite pas vraiment de ce que son titre suggère, mais il constitue une bonne explication de Lisp
Ce n’est pas forcément un problème, mais « reléguer les expressions au rang de syntaxe de seconde classe » est un choix assez fondamental et intentionnel. C’est aussi une conception faite pour compliquer la vie des développeurs qui veulent écrire une soupe de parenthèses
C’est pourquoi
lambda:et:=sont volontairement maladroits afin d’en décourager l’usage, et il n’est pas non plus possible d’écrire des fonctions anonymesPython est un langage qui dit que, si vous voulez faire quelque chose d’intelligent, il faut le faire avec des itérateurs, pas avec des objets appelables
J’aimerais qu’il y ait une absence de syntaxe à la Lisp partout
Pour moi, c’est mieux ainsi, et heureusement c’est facile à faire en CL
Un dialecte de Lisp intégré à Python
https://hylang.org/