Du Lisp

 x0r   0
lisp

Je programme depuis l’âge de sept ans et au cours de ma vie, j’ai eu l’occasion et la curiosité d’essayer et d’utiliser de nombreux langages de programmation. Mais parmi les langages que je connais, le Lisp est celui qui a de loin le plus bousculé ma vision de la discipline.

J’ai découvert le Lisp de manière complètement fortuite il y a deux ans. Je cherchais en fait un outil de mind-mapping et j’étais alors tombé sur Org mode couplé à Spacemacs, lui-même une surcouche pour GNU Emacs.

De fil en aiguille, Emacs m’a fait découvrir Emacs Lisp. À partir de là, j’ai fini par adopter Common Lisp et dans une moindre mesure Clojure, en essayant au passage Haskell (qui n’est certes pas un Lisp, mais semble faire partie de la famille en tant que membre d’honneur). Mais ma dernière découverte, et de loin la plus étonnante, est le langage Racket, que j’étudie actuellement.

Au cours de ces différents essais de langages, ma bien-aimée a essayé et adopté Common Lisp plutôt que le C pour une idée de jeu qu’elle avait depuis plus de dix ans et j’ai aussi fait des prototypes de générateurs de texte à base de chaînes de Markov et un solveur de picross.

La famille Lisp

Le Lisp d’origine date de la fin des années 1950, ce qui fait de la famille des Lisp une des plus anciennes qui existent.

Il est difficile d’illustrer ce qui m’attire dans les Lisp sans donner au moins un exemple de code. En voici un, exprimé en Common Lisp, qui calcule la factorielle d’un nombre n en utilisant une fonction auxiliaire f et une récursion terminale :

(defun factorial (n)
  "Calcule la factorielle du nombre N."
  (flet ((f (acc n)
           (if (<= n 1)
               acc
               (f (* acc n) (1- n)))))
    (f 1 n)))

Je déclare ici une fonction factorial, acceptant un seul paramètre nommé n, accompagné d’une petite description destinée à un système de génération documentaire. J’y introduis une fonction auxiliaire f prenant en paramètre un résultat pratiel acc (pour « accumulateur ») et le nombre dont on calcule la factorielle, toujours appelée n.

Dans cette fonction auxiliaire, si n est inférieur ou égal à 1, le calcul est terminé et on renvoie acc. Sinon, on appelle f récursivement. Avec cette fonction auxiliaire définie, il ne reste plus qu’à l’appeler avec 1 et n comme paramètres pour calculer le résultat.

Ce qui saute immédiatement aux yeux sont la quantité de parenthèses. Il s’agit en fait de la représentation canonique de listes, qui sont délimitées par des parenthèses et dont chaque élément est séparé par un blanc. On dit aussi que ces listes sont représentées sous la forme de S-expressions.

Ainsi, (f (* acc n) (1- n)) est une liste dont les éléments sont, dans l’ordre, le symbole f, la liste (* acc n) et la liste (1- n). Et puisque chaque élément d’une liste peut aussi être une liste, on peut utiliser ces listes pour construire un arbre syntaxique d’un programme.

Voilà donc une des différences fondamentales entre le Lisp et les autres langages de programmation : en Lisp, le code source est une représentation explicite de son arbre syntaxique, alors que dans les autres langages, cet arbre, généralement construit et manipulé dans le compilateur uniquement, est inaccessible de l'extérieur.

Voici un second exemple en Common Lisp qui affiche une liste de courses :

(defun courses (ingredients)
  "Affiche une liste de courses."
  (if (null ingredients)
      (princ "Je n’ai rien à acheter")
      (progn
        (format t "J’ai ~D ~A à acheter : "
                (length ingredients)
                (if (> (length ingredients) 1) "choses" "chose"))
        (labels ((p (ingredients)
                   (format t "~(~A~)" (first ingredients))
                   (case (length ingredients)
                     (1 (princ "."))
                     (2 (princ " et ") (p (rest ingredients)))
                     (otherwise (princ ", ") (p (rest ingredients))))))
          (p ingredients))))
  (fresh-line))

(Note aux connaisseurs : je sais que j’aurais pu me contenter d’un unique appel à la fonction format pour afficher cette liste, mais la chaîne de format deviendrait très vite absconse.)

Appeler cette fonction depuis du code nécessite cependant d’utiliser un opérateur spécial, quote, pour indiquer une donnée à utiliser telle quelle : un symbole qui ne désigne pas une variable ou une liste qui n’est pas un appel de fonction. Ici, il faut que la liste de courses soit traitée comme une liste au lieu d’un un appel de fonction (notez l’apostrophe devant chaque liste, qui est la forme abrégée de quote), comme ceci :

* (courses '())
Je n’ai rien à acheter

* (courses '(œufs))
J’ai 1 chose à acheter : œufs.

* (courses '(œufs chocolat))
J’ai 2 choses à acheter : œufs et chocolat.

* (courses '(œufs chocolat sucre farine))
J’ai 4 choses à acheter : œufs, chocolat, sucre et farine.

Le fait qu’un programme Lisp soit représenté à l’aide de sa propre représentation de listes et d’arbres est aussi appelé homoiconicité. Cette propriété, partagée par très peu de langages de programmation, rend ainsi trivial la génération de code. Par conséquent, Lisp bénéficie d’un système de macros parmi les plus puissants de tous des langages de programmation.

Emacs et Emacs Lisp : ma première expérience de Lisp

J’avais déjà utilisé un peu Emacs quand j’étais en école d’ingénieurs, mais je ne me voyais pas m’en servir autrement qu’après avoir tapé M-x viper-mode car j’avais beaucoup trop l’habitude des touches de vi. Spacemacs me retirait une grosse épine du pied en proposant un Emacs « vimifié » ; j’étais donc prêt à me refaire une nouvelle première impression. Et j’allais avoir une semaine de vacances devant moi pendant lesquelles j’aurais suffisamment de temps pour explorer sérieusement Emacs.

Je me souviens donc bien comment je m’étais installé dehors, un après-midi, avec mon PC portable sur les genoux, pour lire le manuel d’Emacs Lisp.

Les extensions d’Emacs, mais aussi une grosse partie de l’éditeur lui-même, sont codés en Emacs Lisp. Ce langage est un dérivé d’un dialecte assez ancien de Lisp, Maclisp, qui date des années 1960. Ce qui explique certains de ses pièges, comme le fait que les variables aient par défaut une portée dynamique plutôt que lexicale (ce qui est cependant en train de lentement changer). La quantité de code Emacs Lisp qui circule de nos jours signifie que tout changement majeur, comme une forme de programmation concurrente, doit être introduite en procédant à tâtons pour éviter de casser le code existant.

Pour cette raison, les détracteurs disant qu’Emacs a davantage les fonctionnalités d’un système d’exploitation que ceux d’un éditeur de texte ont à mon avis partiellement raison. Emacs est en effet un interpréteur Lisp doté de fonctions conçues pour éditer du texte, mais il est capable de bien plus que ça. Autrement dit, Emacs est une machine Lisp moderne.

J’utilise maintenant énormément Org-mode au travail et je me suis fait quelques petits scripts en Emacs Lisp pour automatiser certaines tâches administratives fastidieuses. Étant contraint à Windows au bureau, système sur lequel Emacs a été porté, j’apprécie beaucoup cet accès facile à un Lisp tout à fait honorable. Et en parallèle, je suis devenu encore plus frustré par les outils informatiques dépourvus du moindre mécanisme d’extension.

Toujours est-il que l’omniprésence d’Emacs Lisp dans Emacs font de cet éditeur un des meilleurs outils pour programmer non seulement en Emacs Lisp, mais dans n’importe quel Lisp de manière générale.

Common Lisp : l’héritier des Lisp traditionnels

Le Common Lisp a été l’étape suivante dans mon aventure. Mon tutoriel a été le livre Practical Common Lisp de Peter Seibel, qu’on peut lire gratuitement en ligne ou acheter en version papier. Et quand on code, le Common Lisp HyperSpec est accessible en ligne et constitue le manuel de référence du langage.

Ce langage est le fruit d’un effort de standardisation d’un Lisp remontant aux années 1980. Ces efforts ont abouti à une norme ANSI publiée en 1994 et restée inchangée depuis. Le but du jeu était de créer un Lisp couvrant les fonctionnalités de plusieurs dialectes incompatibles entre eux. Par conséquent, le langage n’est certes pas aussi « pur » qu’un Scheme et comporte quelques petites incohérences et de petits défauts ; la syntaxe de la macro loop et celle de la fonction format étant les aspects les plus controversés. Néanmoins, il est tout à fait apte à être utilisé pour des applications industrielles et j’ai même vu un livre de finance dont les exemples de code sont en Common Lisp. Paul Graham explique par exemple comment, dans les années 1990, il a monté une start-up d’hébergement de boutiques en ligne, Viaweb, en Lisp, dans son article « Beating the Averages ».

Son écosystème riche, son système de programmation orientée objet, CLOS, ainsi que son système de gestion d’erreurs qui propose une approche intéressante aux exceptions tels qu’on les trouve en C++, Java ou Python font de ce Lisp un langage qui vaut le détour.

Clojure : un Lisp pour la JVM

Clojure, quant à lui, est un Lisp plus récent, conçu pour s’intégrer dans l’écosystème Java. Il s’exécute en effet exclusivement dans la JVM et le langage est bien entendu doté de primitives pour interagir avec des classes Java. En résumé, c’est une façon de faire du Java sans subir la syntaxe verbeuse ni l’orienté objet à outrance du Java.

Son Java interop m’a déjà bien servi : j’avais eu besoin un jour de déboguer un document Word (au format docx), généré par un logiciel mais que Word déclarait corrompu. Pour cela, j’ai utilisé la bibliothèque Java docx4j, quelques fonctions en Clojure mais surtout le REPL de Clojure pour explorer la structure du document et trouver le problème. In fine, il s’avérait juste que deux éléments XML avaient le même identifiant.

Il existe aussi une variante de Clojure appelée ClojureScript, qui comme son nom l’indique tourne non pas sur la JVM mais dans un environnement JavaScript. Clojure et ClojureScript utilisés ensemble permettent donc de partager du code entre les côtés serveur et client dans un projet Web, mais je n’ai pas encore eu l’occasion de mettre cela en pratique. Cela étant, le développement en ClojureScript pour cibler un navigateur est un peu plus fastidieux car l’accès à un REPL est plus compliqué à mettre en place qu’avec Clojure.

Parmi les aspects les plus intéressants du langage figurent les lazy seqs, c’est-à-dire des séquences, pouvant être infinies, qui sont évalués de façon paresseuse. On peut par exemple définir une fonction qui génère la suite (infinie) de Fibonacci, puis n’en demander que le dixième terme ; ceci provoque le calcul des dix premiers termes uniquement.

Mais il y a plusieurs aspects qui me rebutent dans ce langage et qui me font hésiter à en faire mon outil de choix.

Premièrement, du fait de son adhérence forte à Java, certains des reproches qu’on fait à Java s’appliquent aussi à Clojure. Lancer un REPL, par exemple, devient vite lent et gourmand en RAM : dans un projet vierge, ça démarre en une demi-seconde avec déjà 294 Mo de RAM occupés, mais dans un projet plus avancé avec une quinzaine de bibliothèques en dépendances, ça monte à 678 Mo de RAM après près de deux minutes de compilation. Et dans mon environnement Emacs, il n’est pas rare de voir des sous-processus Java monter à plus de 2 Go de RAM !

Clojure me semble plutôt marcher sur les plates-bandes traditionnelles de Java, qui sont les gros logiciels (trop) complexes qu’on démarre et arrête généralement en même temps que sa machine hôte. Je ne me vois pas faire de scripts avec, simplement à cause du temps de démarrage de la JVM qui interdit de fait ce type d’usages.

Deuxièmement, le fait qu’il soit distribué sous la licence Eclipse Public License (EPL) 1.0 me semble être un choix malheureux, étant donné qu’il faut nécessairement distribuer le binaire (.jar) du cœur de Clojure avec tout programme Clojure. Or cette licence connue pour poser problème avec d’autres licences libres comme la GPL.

Le langage est en lui-même intéressant et a peut-être permis d’introduire du Lisp dans des environnements traditionnellement prompts à se ruer sur Java pour tout projet. Mais ce ne sera pas le langage que je choisirai en premier pour un nouveau projet car ça reste une « usine à gaz ».

Haskell, le parfait anti-Lisp dans sa syntaxe

Certains ouvrages sur Lisp que j’ai lus avant d’écrire ce billet mentionnaient Haskell, presque comme s’il s’agissait d’un Lisp lui aussi. Il s’agit d’un langage fonctionnel, une approche qui se prête bien au Lisp. Par curiosité, j’ai voulu essayer.

Le tutoriel Web sur le site officiel, entièrement interactif, m’a particulièrement impressionné. Une autre très bonne ressource qui m’a servi pour essayer le Haskell est le livre Learn You a Haskell for Great Good! de Miran Lipovača (disponible gratuitement en ligne).

Mais j’ai du mal avec sa syntaxe. La quasi-absence de parenthèses est très déroutante car elle oblige le lecteur à se rappeler des règles d’associativité. Les appels de fonction n’échappent pas à la règle ; le langage doit donc prévoir un opérateur, $, pour appliquer une fonction avec une associativité à droite plutôt qu’à gauche ; le tout est un obstacle à un code intelligible. Ensuite, la nécessité d’indenter le code de façon très précise, ce qui revient à donner une importance syntaxique aux blancs (comme en Python), est un autre aspect avec lequel j’ai personnellement beaucoup de mal. Le haskell-mode d’Emacs facilite un peu les choses, mais j’ai tout de même le sentiment que ce langage n’est pas fait pour moi.

Néanmoins, je perçois l’influence du Lisp dans le fait que l’opérateur servant à ajouter un élément au début d’une liste (:) s’appelle « cons », comme la fonction du même nom en Lisp.

J’accorderai peut-être une seconde chance plus tard à ce langage, mais dans l’immédiat, je préfère le confort et l’inambiguïté des S-expressions.

Retour à Common Lisp

En recherchant des ressources sur la programmation en Emacs Lisp, je suis tombé sur un petit tutoriel ludique dans lequel on programme un petit jeu d’aventure textuel en Lisp. Ce tutoriel est par ailleurs une bonne démonstration de ce qu’il est possible de construire avec des macros Lisp.

L’auteur, Conrad Barski, a ensuite publié le très bon ouvrage Land of Lisp, qui va dans le prolongement de ce petit tutoriel : tous les exemples sont des jeux. Cette approche convient particulièrement bien à un public non informaticien et permet d’aborder des thématiques que je ne ne vois pas souvent dans des introductions à des langages de programmation, comme par exemple la programmation d’intelligences artificielles pour des jeux de plateau. Grâce à ce livre, Nausicaa a adopté le Common Lisp et est en train d’écrire un jeu dans ce langage ; ce qui est de très bonne augure, car cela nous fait un langage de programmation en commun que nous apprécions ensemble.

Peu avant de partir en vacances d’été, je me suis amusé, en guise de petit défi, d’écrire un solveur de picross. J’avais choisi Common Lisp plutôt que Clojure car Common Lisp ne dispose pas des lazy seqs de Clojure. C’est un projet qui s’est avéré beaucoup plus intéressant qu’à première vue et dont je parlerai sûrement dans un billet futur.

Par ailleurs, Common Lisp étant lui-même une spécification, il existe en fait plusieurs environnements Lisp qui portent des noms différents : parmi eux, il y a GNU CLISP, SBCL, Clozure CL (à ne pas confondre avec Clojure) et ABCL, ce dernier ayant la particularité de tourner sur la JVM.

Racket ou la programmation orientée langages

Ma dernière destination dans mon voyage dans l’univers des Lisp est Racket.

Racket est en fait un langage dérivé de Scheme, qui est lui-même un Lisp qui a vu le jour dans les années 1970 au MIT. Racket est conçu à la fois comme un outil de recherche et comme un support pédagogique en théorie de langages de programmation (d’où son nom d’origine, PLT Scheme – PLT pour Programming Language Theory).

La principale caractéristique de Racket est d’offrir un cadre pour définir, dans le compilateur Racket, n’importe quel langage dédié. Bien que les autres Lisp se prêtent très bien à la création de tels langages dédiés, ces langages sont la plupart du temps tenus à respecter la syntaxe des S-expressions. Racket, quant à lui, permet de créer des langages qui s’affranchissent de cette limitation : ainsi, on peut très bien créer des langages où on peut exprimer le produit 7 × 191 par une notation infixe (7 * 191) plutôt que préfixe ((* 7 191)).

Ainsi, Racket pousse la notion de programmation orientée langages le plus loin possible. Plutôt que d’exprimer une solution à un problème directement dans un langage de programmation, on définit d’abord un langage plus concis dans lequel on exprime le problème, on écrit un programme transformant les expressions de ce nouveau langage en code Racket, puis on compile ce nouveau langage.

La programmation orientée langages a lui-même de nombreuses applications. Il existe par exemple un langage d’édition vidéo. Le studio Naughty Dog a également utilisé des mini-langages créés dans Racket pour scripter l’intrigue et l’intelligence artificielle dans certains de ses jeux (voir par exemple cette présentation par le CTO de l’entreprise).

J’ajouterais aussi que même si personnellement, je préfère Emacs et son racket-mode pour des raisons de confort, l’IDE de référence, DrRacket est bien conçu et est un outil qu’on peut mettre entre toutes les mains, notamment de débutants. En particulier, le livre How to Design Programs par Matthias Felleisen et al. utilise Racket comme outil pédagogique.

Je n’ai pas encore eu l’occasion de me faire la main sur Racket avec un projet sérieux, mais je vois énormément de potentiel dans un langage qui a l’air à la fois très puissant et très accessible à des débutants.

Conclusion

Ma découverte fortuite d’Emacs m’a permis d’explorer en deux ans un univers entier de langages de programmation que je n’aurais pas eu la curiosité d’étudier sinon. Je suis conscient que je n’ai pas encore exploré Scheme, en dépit de ma possession d’un exemplaire papier de Structure and Interpretation of Computer Programs de Harold Abelson et Gerald J. Sussman (consultable gratuitement en ligne), ni Guile, entre autres utilisé pour scripter divers logiciels GNU. Mais je pense néanmoins avoir au moins fait le tour des langages les plus représentatifs de ce qu’est la famille Lisp aujourd’hui.

Ces nouveaux langages pour moi ont été l’occasion de me lancer dans certains projets personnels dont je parlerai sûrement dans d’autres billets de blog : par exemple, j’ai fait un robot générateur de texte à base de chaînes de Markov ou un solveur de Picross assez efficacement ; dans les deux cas, mon tout premier prototype était opérationnel en l’équivalent d’une ou deux soirées.

Me voilà donc converti au Common Lisp, voire peut-être au langage Racket si mes expériences dans ce langage-là sont concluants.

Pour terminer, je me permets de citer Eric S. Raymond, qui, dans son essai intitulé « How To Become A Hacker », écrit :

LISP is worth learning for a different reason — the profound enlightenment experience you will have when you finally get it. That experience will make you a better programmer for the rest of your days, even if you never actually use LISP itself a lot.

Commentaires

Poster un commentaire

Poster un commentaire