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.