Puisqu’on est en 2023, autant apprendre Common Lisp
(log.schemescape.com)- Dans une réflexion sur les langages utilisables dans le futur, ce choix relève moins de la praticité que de l’envie d’expérimenter directement un workflow de développement ancien, mais moderne
- Janet est un petit langage de script de la famille Lisp/Clojure, visant un créneau proche de Lua, qui a suscité l’intérêt grâce à sa bibliothèque standard et à la métaprogrammation à base de macros
- Les atouts perçus de Janet — portabilité, possibilité d’embedding, Parsing Expression Grammars — ne correspondaient que partiellement aux besoins actuels, tandis que des options existantes comme CHICKEN Scheme, CLISP, SBCL ou Packrat semblaient déjà suffisantes
- Le point central du développement piloté par le REPL de Common Lisp est la possibilité, lors d’une erreur à l’exécution, d’inspecter la pile, les variables et les données live, de les modifier, puis de reprendre l’exécution interrompue
- Même s’il faut apprendre un nouveau langage et sa bibliothèque standard, l’auteur estime que ce workflow mérite d’être expérimenté directement dans un environnement conçu pour cela dès le départ
Pourquoi l’intérêt s’est déplacé de Janet vers Common Lisp
- L’auteur réfléchissait aux future-proof programming languages parce qu’il voulait écrire du code encore utilisable à l’avenir, mais lorsqu’il s’agit de créer quelque chose et de le partager, il adopte une attitude pragmatique en choisissant un langage populaire, portable et pratique
- En parallèle, il explore aussi d’autres langages et outils de programmation par plaisir et pour expérimenter
- Récemment, la découverte du livre gratuit Janet for Mortals a suscité son intérêt pour Janet
- Janet est un langage de script relativement petit, inspiré de Lisp/Clojure, qui vise un créneau similaire à Lua
- Il fournit une vraie bibliothèque standard, de la métaprogrammation à la Lisp et l’exécution à la compilation via des macros
- Au-delà de son côté Lisp en général, ce qui attirait le plus dans Janet était sa portabilité, sa capacité à être embarqué et les Parsing Expression Grammars
- Comme il n’a actuellement pas besoin d’embarquer un langage, cette capacité n’était pas un critère décisif
- Pour la portabilité, CHICKEN Scheme, CLISP et Steel Bank Common Lisp semblaient aussi de bonnes options
- Pour le parsing, Packrat semblait être un choix raisonnable
- Au final, les raisons de choisir Janet plutôt qu’un Lisp existant se sont réduites
Le workflow piloté par le REPL qui a donné envie d’apprendre Common Lisp
- Un article consacré au workflow piloté par le REPL, presque propre à Common Lisp, a motivé cet apprentissage
- L’exemple décrit une fonction
fooqui appelle une fonctionbarpas encore définie, ce qui provoque une erreur pendant l’exécution defoo- Dans les anciens environnements Lisp ou Smalltalk, l’interruption de
foomène à une breakloop - La breakloop est un REPL complet situé dans l’environnement dynamique de la fonction interrompue
- On peut parcourir la pile d’appels interrompue et inspecter les variables visibles dans chaque frame de pile
- On peut examiner et modifier les données live du programme en cours d’exécution
- Si la cause vient d’une variable ou d’une valeur de champ incorrecte, on peut changer la valeur puis reprendre la fonction interrompue
- On peut définir la fonction
barmanquante depuis le REPL, puis reprendrefoopour obtenir un résultat raisonnable
- Dans les anciens environnements Lisp ou Smalltalk, l’interruption de
- L’auteur avait déjà vu plusieurs fonctionnalités de type « edit and continue », mais il a trouvé nouveau le fait que Common Lisp ait été conçu en pensant à ce genre de workflow
- Son expérience du débogage a évolué du
printf debuggingvers l’utilisation de vrais débogueurs- Le
printf debuggingconsiste à ajouter temporairement du code de log, recompiler, puis vérifier le résultat de l’exécution - Dans les situations où il n’est pas possible de modifier ou de relancer le programme, il fallait apprendre à utiliser un débogueur
- Un vrai débogueur est le bon outil, mais configurer l’environnement de débogage peut être pénible, voire parfois impossible
- Le
- Common Lisp lui a semblé être un environnement qui pousse le débogage un cran plus loin
- Il avait déjà modifié la mémoire depuis un débogueur pour tester des correctifs potentiels
- Mais la possibilité de réécrire et de patcher du code dans un processus en cours d’exécution était particulièrement séduisante
- L’objectif de l’apprentissage de Common Lisp est de vérifier directement si construire des programmes de manière interactive constitue une façon agréable de travailler
- Il n’est pas certain qu’apprendre un nouveau langage et sa bibliothèque standard soit le meilleur choix pour explorer un nouveau workflow, mais aucune meilleure alternative ne s’impose clairement
- D’autres langages peuvent aussi proposer une intégration similaire entre REPL et éditeur
- Mais l’auteur estime que ces fonctionnalités, ajoutées après coup plutôt que prises en charge dès le départ, risquent d’être plus boguées
- Même en cas d’insatisfaction, il restera le fait d’avoir essayé dans les meilleures conditions, avec les outils standard
- Les ressources utiles pour apprendre Common Lisp sont rassemblées dans un article séparé, Common Lisp resources
1 commentaires
Commentaires sur Hacker News
Jusqu’ici, ce que j’ai apprécié dans Common Lisp, c’est que le système de conditions est propre, et que les redémarrages (restarts) permettent de contrôler facilement du code situé loin dans le programme. Le développement piloté par le REPL est aussi pratique quand on ne sait pas très bien ce qui va se passer, par exemple lorsqu’on parse une source de données inconnue : on peut corriger le code et continuer sans perdre le contexte.
Il existe beaucoup d’implémentations, avec une bonne interopérabilité ; dans un cas, j’ai pu alterner entre SBCL pour la vitesse et CLISP pour réduire l’usage mémoire. L’une des raisons pour lesquelles j’ai penché vers CL plutôt que Scheme pour apprendre Lisp est aussi l’existence de plusieurs implémentations compatibles.
Même en tant que débutant sur Emacs, j’ai trouvé l’intégration avec Common Lisp excellente, et elle tourne bien même sur le netbook très lent que j’utilise pour développer. À l’époque des ordinateurs rapides, de VS Code et des serveurs de langage, c’est un avantage moins décisif, mais il y a clairement un côté rétrofuturiste.
Il y a aussi des points décevants. Le gestionnaire de paquets le plus populaire, QuickLisp, est correct, mais il n’atteint pas le niveau de fonctionnalités auquel les écosystèmes de langages modernes nous ont habitués. Le langage lui-même étant figé dans le temps, beaucoup de choses — threads, synchronisation, arguments de ligne de commande, etc. — nécessitent des bibliothèques d’interopérabilité. Et j’aimerais vraiment que SBCL prenne en charge les builds entièrement statiques, afin de pouvoir distribuer des binaires aussi sur des distributions Linux qui n’utilisent pas glibc.
Si un outil ne prend pas en charge le chiffrement ou l’authentification pour une opération aussi critique que récupérer du code source, je pense que QL peut difficilement être une option pour les personnes soucieuses de sécurité.
Le fait que SBCL soit hébergé sur SourceForge pose aussi problème. SourceForge a déjà injecté par le passé des logiciels malveillants dans des archives de téléchargement, donc j’y vois également un enjeu de sécurité. Je ne vois pas de bonne raison de continuer à utiliser SourceForge aujourd’hui, et je ne comprends pas bien pourquoi un projet important y reste encore.
J’aime vraiment Lisp, et Common Lisp en particulier, mais ce genre de choses m’en a éloigné, et il ne semble pas y avoir beaucoup d’intérêt pour les corriger.
https://www.timmons.dev/posts/static-executables-with-sbcl.h...
https://www.timmons.dev/posts/static-executables-with-sbcl-v...
https://cons.io
Gerbil/Gambit Scheme peut générer des binaires entièrement statiques comme alternative à CL.
On peut l’utiliser comme une bibliothèque Quicklisp classique, comme une image core qui démarre immédiatement, ou comme un binaire. Les scripts peuvent aussi s’exécuter presque instantanément, ce qui le rapproche un peu de Babashka. Les détails sont encore en cours de finition et ce n’est pas encore une v1.0.
La partie du blog sur « gérer une erreur d’exécution en corrigeant sur place le code cassé, sans redémarrage » aurait dû être montrée ici : https://www.youtube.com/watch?v=jBBS4FeY7XM
Si vous lancez un calcul long et coûteux et qu’une erreur survient à la toute dernière étape, au lieu de tout relancer depuis le début, vous entrez dans le débogueur interactif, allez à la ligne en erreur, compilez la fonction corrigée, puis revenez au débogueur et reprenez l’exécution à un point donné de la frame de pile. Ensuite, vous pouvez voir le programme passer.
M-me paraît bien meilleur que ce que Clojure propose avectap>. Même avec des outils comme Portal, je trouve quetap>ressemble surtout à un pretty-printing enjolivé.Côté performances aussi, Common Lisp permet de spécifier des types statiques, et SBCL peut alors produire du code très efficace. Lancer
DISASSEMBLEsur son propre code pour voir ce qui est réellement émis et l’optimiser est utile. En particulier, des paquets comme SB-SIMD et Loopus aident énormément pour les applications de calcul numérique.:quackvaut true, traite-le comme un canard », ce qui convient bien à une conception de jeu de type entity-component-system.Mais la manière de développer en Common Lisp, et surtout le système de conditions, m’ont vraiment manqué au cours de l’année écoulée. Je ne suis même pas un hacker particulièrement à l’aise en CL, mais ça s’est très vite ancré dans mes habitudes. J’aimerais que CLOS et les types primitifs de CL s’accordent mieux qu’ils n’en ont l’air.
Avec Cider, c’est clairement facile à faire, et les principaux outils des autres éditeurs doivent probablement le faire suffisamment bien aussi.
DISASSEMBLE.Il existe des projets auxiliaires comme https://github.com/Bronsa/tools.decompiler, et aussi JitWatch d’OpenJDK (https://github.com/AdoptOpenJDK/jitwatch). D’autres JVM ont des outils similaires.
Ce n’est pas aussi intuitif qu’avec Lisp, mais ça reste possible.
C’est bien d’avoir créé quelque chose d’ésotérique qui fonctionne de façon séduisante, mais désormais, personne ne voudra y toucher à part des gens dont le salaire peut s’écrire avec un nombre de zéros.
https://www.digitalocean.com/community/tutorials/java-repl-j...
https://github.com/waf/CSharpRepl
https://pub.dev/packages/interactive
Sans parler de Ruby, Python, PHP, Lua, et même C++ a un REPL bancal : https://github.com/root-project/cling
Chaque fois que je dis que le REPL n’est plus un facteur différenciant pour Lisp, je me fais downvoter.
https://github.com/kaveh808/kons-9
Mais ces redéfinitions et écrasements finissent par polluer les namespaces, et à un moment quelque chose casse, ce qui oblige à recharger le serveur.
Il a beaucoup de défauts, mais ils ne sont pas pires que les défauts purement techniques de presque tous les autres langages et environnements de développement. Python et JS sont plus largement utilisés et ont plus de bibliothèques, mais l’expérience de développement me paraît bien pire. J’ai beaucoup utilisé C#, TS, Py, Hs et des langages encore plus ésotériques pendant des années, mais chaque fois que je m’enlise dans ces langages ou que je suis sérieusement agacé par des choses manifestement mauvaises, je reviens à CL(SBCL + Emacs + Slime). Alors je me détends, et je retrouve la conviction qu’il existe encore de bonnes choses en ce monde.
En ce moment, nous levons des fonds pour un produit que nous avons bêtement bootstrapé en TypeScript, mais nous prévoyons de refaire la version de lancement en CL. Cela signifie que pendant les 3 à 5 prochaines années avant la sortie, je travaillerai avec CL, dans CL, et je suis heureux à l’idée de faire des choses amusantes comme implémenter des DSL, générer du code, travailler avec des macros et implémenter un interpréteur de types statique.
Quand vous travaillez en C#, qu’est-ce qui vous manque dans Lisp ?
Mon idée radicale, c’est que si la programmation fonctionnelle n’a pas décollé à grande échelle, c’est parce qu’elle convient certes à l’écriture de programmes, mais que la plupart des logiciels actuels ne sont pas des « programmes lancés en ligne de commande sur mon PC/serveur et qui tournent jusqu’à terminer », mais plutôt des « programmes qui démarrent, réagissent aux entrées utilisateur, puis se ferment quand l’utilisateur les ferme », ou des « programmes qui démarrent, répondent au réseau ou à d’autres I/O automatiques, puis s’arrêtent quand un autre logiciel leur demande de s’arrêter ».
Ces tâches sont beaucoup plus difficiles à réaliser dans un style purement fonctionnel, ou du moins c’est ce que j’ai constaté dans les implémentations fonctionnelles très opinionated que j’ai essayées. On n’évalue plus simplement une expression : on initialise de l’état, on réagit à des I/O, on met à jour l’état ou on effectue des I/O supplémentaires, et on peut même utiliser du parallélisme pour surveiller, recevoir, effectuer d’autres I/O et mettre à jour l’état.
Bien sûr, faire cela en Lisp n’est pas impossible, mais d’après mon exposition à la programmation fonctionnelle pendant quelques semestres à l’université, et mon usage professionnel de fonctionnalités fonctionnelles en C++/Scala pour résoudre ce genre de problèmes, il m’a semblé assez difficile de faire bien fonctionner la programmation fonctionnelle dans ce type d’applications, et cette inadéquation m’a rendu hésitant à m’y engager davantage.
Indépendamment de Common Lisp, j’ai souvent eu à écrire du code qui génère du code. C’est un domaine où Lisp semble pouvoir briller, mais je n’ai pas encore eu l’occasion d’essayer. Parmi les projets récents menés avant de penser à Common Lisp, il y a par exemple la génération de code validant un JSON Schema spécifique dans un générateur de site statique, ou la génération de JSX à partir de Markdown pour l’utiliser dans le contenu narratif d’un jeu de programmation
Sans parler des innombrables macros C que j’ai utilisées jusqu’ici
Il n’est pas nécessaire non plus de jeter toutes les bonnes fonctionnalités d’autres styles. Certaines parties de la programmation fonctionnelle deviennent de plus en plus courantes même dans les langages généralistes. Par exemple, Rust utilise aussi souvent des patterns fonctionnels
Et Lisp peut aussi être utilisé dans un style orienté objet ou impératif si on le souhaite. Ce n’est pas Haskell
Je considère Lisp comme un point de départ, et Haskell comme le vrai sujet. Je recommande d’en apprendre le style, même si vous n’écrivez jamais de code de production réel avec
(let ((pair (cons 1 nil)))(setf (cdr pair) pair)(list (first pair) (second pair) (third pair)));; => (1 1 1)Si vous voulez que ça « compile pour toujours », écrivez-le en ANSI C
Si vous voulez que ça « survive pour toujours », écrivez-le en Python 2.7 et faites-en la colonne vertébrale des templates d’infrastructure de toute l’organisation. Points bonus si c’est un module Ansible custom
Ce que j’attends de Lisp, c’est qu’il minimise ou abstraie le bazar et le boilerplate pour faciliter l’expression des idées en code. Pour moi, les expressions Lisp semblent être la manière la plus naturelle de représenter des idées
Comme je m’intéresse surtout à l’écriture d’applications GUI, j’aimerais apprendre Clog, ou trouver, voire développer, un bon wrapper au-dessus d’un toolkit GUI
(do-it (do-it first))(do-it first)fonctionne bien, mais que se passe-t-il si l’appel à(do-it (do-it first))échoue ?Ce serait bien de récupérer le contrôle au point cassé et de pouvoir corriger la définition de
do-it. Mais si on la corrige, le résultat de(do-it first)change aussiAlors l’endroit où se trouve actuellement la machine devient un point qui n’est plus atteignable avec le code actuel
Je ne vois vraiment pas comment ça fonctionne si la modification qui permet de continuer change elle-même l’état dans lequel on a reçu le contrôle pour effectuer cette modification