3 points par GN⁺ 2023-07-28 | 1 commentaires | Partager sur WhatsApp
  • 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
    • Il suivait des langages intéressants aux noms courts comme Nim et Zig
    • Ce type d’expérimentation peut aussi faire découvrir de nouvelles façons de programmer ou des outils qui deviendront indispensables plus tard
  • 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 foo qui appelle une fonction bar pas encore définie, ce qui provoque une erreur pendant l’exécution de foo
    • Dans les anciens environnements Lisp ou Smalltalk, l’interruption de foo mè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 bar manquante depuis le REPL, puis reprendre foo pour obtenir un résultat raisonnable
  • 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 debugging vers l’utilisation de vrais débogueurs
    • Le printf debugging consiste à 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
  • 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

 
GN⁺ 2023-07-28
Commentaires sur Hacker News
  • Je ne pensais pas voir mon article ici. J’aimerais écrire une suite un jour, mais pour l’instant je suis encore débutant.
    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.
    • Il y a aussi quelques inconvénients. La dernière fois que j’ai vérifié, QuickLisp récupérait les paquets uniquement en HTTP en clair, sans chiffrement ni mécanisme de vérification permettant de détecter une modification pendant le transport.
      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.
    • Si vous avez besoin de builds statiques et que vous pouvez utiliser une version un peu ancienne de SBCL, le travail de daewok, qui a buildé et linké SBCL dans un environnement musl, pourrait être la solution que vous cherchez. J’ai essayé de porter les patchs vers une version plus récente, mais à cause de changements upstream il reste un défaut de segmentation.
      https://www.timmons.dev/posts/static-executables-with-sbcl.h...
      https://www.timmons.dev/posts/static-executables-with-sbcl-v...
    • Je vais vous donner un cons.
      https://cons.io
      Gerbil/Gambit Scheme peut générer des binaires entièrement statiques comme alternative à CL.
    • Je suis en train de créer CIEL pour les cas où l’on veut démarrer avec un bon ensemble de bibliothèques (JSON, CSV, bases de données, client HTTP, arguments de ligne de commande, extensions du langage, etc.) : https://github.com/ciel-lang/CIEL/
      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.
  • Au travail, j’utilise Clojure, mais chaque fois que je dois déboguer ou que j’ai besoin de code rapide, presque tout dans Common Lisp me manque. Pouvoir, au milieu d’erreurs imbriquées, cliquer sur n’importe quelle partie de la pile pour inspecter les liaisons lexicales est extrêmement utile, et cliquer sur un objet puis le copier dans le REPL avec M- me paraît bien meilleur que ce que Clojure propose avec tap>. Même avec des outils comme Portal, je trouve que tap> 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 DISASSEMBLE sur 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.
    • Cela résume bien les frustrations que j’ai ressenties en écrivant mon mémoire de master en Clojure. Je ne regrette pas forcément ce choix. Clojure combine bien l’impression que « tout est une map clé-valeur » avec l’approche « si la clé :quack vaut 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.
    • En Clojure, flowstorm mérite qu’on s’y intéresse. On peut avancer et reculer pas à pas dans une fonction, et envoyer des maps vers le REPL avec ces fonctions.
    • Dans aucun des deux langages, l’envoi de forms vers le REPL n’est fourni par le langage lui-même ; pour moi, c’est une fonctionnalité des outils.
      Avec Cider, c’est clairement facile à faire, et les principaux outils des autres éditeurs doivent probablement le faire suffisamment bien aussi.
    • En Clojure aussi, on peut faire dans une certaine mesure quelque chose de similaire à 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.

  • Steel Bank Common Lisp est le cheval de trait qui nous a permis de créer une société logicielle rentable. Sans lui, je ne pense pas que nous aurions été aussi productifs. Le workflow piloté par le REPL est excellent, les images Lisp sont très robustes, et les performances sont élevées.
    • Pour les curieux, pouvez-vous partager de quelles sociétés il s’agit ?
    • Ça a l’air génial, mais aujourd’hui je suis trop paresseux pour revenir à Emacs. Pour le développement Python et JavaScript, j’utilise surtout VSCode avec une configuration quasiment par défaut. Comme je suis CTO, je ne code pas à plein temps.
    • Je pense que l’époque où la stack technique avait ne serait-ce qu’un peu d’importance est en grande partie révolue.
      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.
    • On dit que le workflow piloté par le REPL est incroyable, mais j’ai l’impression que certains ignorent qu’aujourd’hui, pratiquement tous les langages sur machine virtuelle/interpréteur ont un REPL.
      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.
  • Si vous vous intéressez à la 3D graphique, il existe une base de code Common Lisp qui vaut le coup d’œil. J’ai essayé de la garder simple et facile à comprendre.
    https://github.com/kaveh808/kons-9
    • Cherchez les tutoriels CL de Kaveh sur YouTube. Ils sont vraiment très bien faits.
  • En tant que développeur Clojure, le break loop et le workflow piloté par le REPL ont l’air super, et je pense qu’ils seraient clairement utiles à Clojure. Cela pourrait le rapprocher davantage du codage frontend JS/TypeScript avec les excellents outils de débogage du navigateur. Malheureusement, l’état actuel des outils et du soutien communautaire dans l’écosystème Clojure semble assez faible.
    • Clojure peut aussi imiter dans une certaine mesure un vrai flux REPL lorsque les appels profonds dans les couches de code se produisent à chaque requête, comme dans un serveur web. Pendant que le serveur continue de tourner, on peut réécrire et recharger plusieurs fonctions, puis refaire une requête depuis le navigateur.
      Mais ces redéfinitions et écrasements finissent par polluer les namespaces, et à un moment quelque chose casse, ce qui oblige à recharger le serveur.
  • J’aime CL et il me manque vraiment quand je fais autre chose. Comparé à mon expérience avec Lisp il y a des décennies, beaucoup de choses dans les langages « modernes » sont si pénibles que ce n’en est même pas drôle.
    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.
    • Un Lisp sans cons, ça n’existe pas.
      Quand vous travaillez en C#, qu’est-ce qui vous manque dans Lisp ?
    • Pourquoi TypeScript est-il inadapté à ce produit ?
  • Dans le blog et les commentaires de l’auteur ici, il est beaucoup question de « coder », mais on voit très peu quel type de logiciel est construit et quels cas d’usage sont visés.
    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.
    • Bon point. Pour l’instant, tout cela n’est qu’un hobby. Ce que j’ai fait en Common Lisp, c’est un client Battlesnake ordinaire et un jeu multijoueur de mélange de mots qui a fermé hier. Aucun des deux n’a vraiment tiré un grand avantage du fait d’être écrit en Lisp, mais j’ai beaucoup appris au passage, et c’était l’essentiel.

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

  • Common Lisp intègre encore aujourd’hui l’un des systèmes orientés objet les plus avancés, CLOS. La plupart des Lisp ne sont pas des langages fonctionnels comme Haskell
  • Elixir utilise la programmation fonctionnelle et excelle quand on a besoin de développement web ou de systèmes tolérants aux pannes
    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
  • Il m’a fallu un certain temps pour comprendre les monades et la monade IO, et encore plus longtemps pour apprendre à les composer en toute sécurité et à manipuler l’ordre d’exécution. Mais maintenant, je peux écrire des applications typées de façon sûre et je fais moins de bugs même quand je travaille dans des langages non fonctionnels. Le langage que j’utilise contre rémunération, c’est Java
    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
  • Common Lisp n’est pas un langage « purement fonctionnel » : il prend en charge tous les paradigmes. Il autorise même ce genre de bizarrerie
    (let ((pair (cons 1 nil)))
    (setf (cdr pair) pair)
    (list (first pair) (second pair) (third pair)))
    ;; => (1 1 1)
  • Si vous voulez que ça « s’exécute pour toujours », écrivez-le en Rust
    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
  • Comme dans « nous sommes en 2023, donc évidemment j’apprends Common Lisp », je suis moi aussi en train d’apprendre, mais les fonctionnalités mentionnées ne me paraissent pas encore utiles. Peut-être que je changerai d’avis en m’y habituant
    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
  • L’essentiel, c’est que Scheme et Janet sont aussi excellents, mais que l’auteur veut un langage plus autonome. Ce qui fait la différence, c’est le breakloop : un REPL complet qui s’ouvre lorsqu’une erreur se produit dans le programme. Ce n’est ni une trace de pile ni un débogueur, mais une façon de construire directement à partir du point actuellement cassé
    • Ça a l’air vraiment incroyable ; alors pourquoi Common Lisp n’est-il pas le langage le plus populaire ? Je pose la question en tant que personne qui écrit très peu de code
    • En tant que développeur Clojure, ça m’intéresse, mais je me demande ce qui se passe dans le cas suivant. Ce n’est pas du code idiomatique Lisp, c’est juste un exemple pour montrer le point que je ne comprends pas
      (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 aussi
      Alors 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
    • Entendre qu’un REPL complet s’ouvre au point de l’erreur, et qu’il ne s’agit pas d’une trace de pile ni d’un débogueur mais de construire directement depuis le point cassé, me donne envie d’ouvrir une petite image Smalltalk
    • C’est aussi standard dans Gambit Scheme
    • Ce genre de choses ne devient-il pas, en production, un vecteur d’attaque pour du code malveillant ou des failles de sécurité ?