À la découverte d'Emacs (Lisp) 24

(Nhat Minh Lê (rz0) @ 2012-05-14 18:46:03)

Depuis quelques jours, j’ai décidé de migrer vers Emacs 24, la version en développement d’Emacs. Le changement s’est effectué sans trop de difficultés, une ou deux extensions mineures devenues incompatibles, quelques options de configuration qui ne font plus tout à fait ce que je veux, mais globalement, une mise à jour tout en douceur, comme d’habitude… après tout, un ancêtre comme Emacs ne doit plus tellement évoluer… si ?

Bah, en fait… non, pas vraiment ; côté fonctionnalités, Emacs 24 n’apporte rien de révolutionnaire, à vrai dire… des améliorations des modes existants : une meilleure prise en charge des gestionnaires de versions distribués, la possibilité de manipuler des fichiers 7-zip, ou encore une meilleure indentation pour certains modes de programmation, par exemple. Il faut aller chercher du côté du programmeur pour trouver les nouveautés de cette version, car il y en a ! Du moins, quelques unes qui ont retenu mon attention, et qui constituent, selon moi, tout autant de petits signes qu’Elisp mûrit encore, lentement mais sûrement, en tant que langage.

Cela ne surprendra probablement personne si je dis qu’Elisp — Emacs Lisp, le dialecte de Lisp utilisé par Emacs, n’est pas tout à fait dans l’air du temps… c’est un Lisp historique, auquel il manque toutes sortes de petits plus qui contribuent à notre confort. Bien qu’il possède des fermetures et des fonctions d’ordre supérieur, on ne peut pas dire qu’Elisp encourage vraiment un style de programmation fonctionnel, tel que ses partisans le défendent aujourd’hui… pas de portée lexicale pour les symboles, pas de types algébriques, ni de récursion terminale optimisée, bref, pas exactement idéal pour faire du fonctionnel…

Du moins, cela a toujours été le cas… jusqu’à Emacs 24. Non, non, je vous rassure, il n’y a toujours pas de typage algébrique en Elisp, mais cette version apporte son lot de changements intéressants qui font d’Elisp un langage un peu plus respectable aux yeux des vrais, des durs. Tour d’horizon des choses nouvelles et moins nouvelles.

Je profite de l’occasion pour parler un peu plus généralement d’Elisp comme langage de programmation ; j’évoque, bien évidemment, les nouveautés d’Emacs 24, mais également des choses plus générales qui pourraient intéresser les curieux, ou les non-initiés, bref, les innocents qui voudraient se lancer dans scriptage de leur Emacs.

Visite guidée : à votre droite, les fondations…

Flots de contrôle

Parmi les constructions de base du langage, rien de nouveau dans Emacs 24 (ou 23, ou 22, etc.) ; on retrouve les conditionnelles du Lisp, if (et ses variantes when et unless) et cond, ainsi qu’une simple boucle while, implémentée comme une primitive dans Elisp. Comme dit plus haut, la récursion terminale n’est pas prise en charge en temps constant, et il vaut mieux utiliser une boucle quand on peut.

Le module cl-macs apporte son lot de macros complémentaires empruntées à Common Lisp (mais utilisées dans quasiment tous les paquetages Emacs), notamment do et ses variantes (dolist et dotimes) ainsi que le tout-puissant loop (dans une forme quelque peu amoindrie).

Le problème des macros Common Lisp dans Emacs est qu’elles sont généralement très succinctement documentées, et le manuel Elisp n’en parle pas. Il faudra donc regarder ailleurs pour apprendre à s’en servir…

Variables, fonctions et fermetures

Côté variables, depuis Emacs 24, Elisp a enfin renoué avec le vingt-et-unième siècle et propose désormais des variables à portée lexicale ! À activer via un petit commentaire en haut du fichier source (il s’agit de mettre la variable lexical-binding, locale au fichier à t), celles-ci se comportent un peu comme en Common Lisp : les variables définies globalement (avec defvar) sont à portée dynamique, tandis que les constructions locales telles que let et letrec introduisent des variables lexicales.

-*- lexical-binding: t -*-
;; À mettre en première ligne de vos fichiers Elisp, avec les autres
;; variables locales au fichier.

Assez traditionnellement, les variables en Elisp sont mutables, comme en Common Lisp ou en Scheme, mais pas comme en ML. L’affectation se fait avec setq ; on trouve setf dans cl-macs, mais il n’est pas d’usage de l’employer à la place de setq.

En ce qui concerne les fonctions, il faut tout d’abord mentionner qu’Elisp est un Lisp-2, ce qui signifie que les noms de fonctions existent dans un espace différent de ceux des variables (on peut avoir une variable list et une fonction list). La déclaration au niveau global se fait par defun ; localement, on dispose des lambda-expressions avec le mot-clé lambda, qui introduit une fonction anonyme.

Les macros Common Lisp labels et flet n’ont malheureusement pas l’air d’avoir été mises à jour pour profiter de la nouvelle prise en charge de la portée lexicale, et utilisent encore l’ancienne émulation via lexical-let — en fait, il n’est pas clair si ce nouveau mécanisme de portée lexicale s’applique également aux fonctions (a priori, ce n’est pas le cas, puisque seul let expose cette nouvelle fonctionnalité, et celui-ci n’affecte pas l’espace des fonctions). Étant donné que labels et flet ne profitent pas du nouveau mécanisme de portée lexicale, la déclaration de fonctions (lexicalement) locales doit se faire préférablement par le biais de let ou letrec, comme suit :

(letrec ((fun
          (lambda (args...)
            body...)))
  body...)

Pas exactement ce que j’appellerais élégant, surtout que cela oblige à utiliser funcall pour l’appel, mais on va dire que l’on fait avec… Je m’attends à ce qu’à mesure que les programmeurs adoptent la portée lexicale, l’usage en devienne plus souple, poussant ainsi les développeurs d’Emacs à ajouter ce genre de support dans une révision future.

Structures de données

Comme tout langage de haut niveau qui se respecte, Elisp prend en charge nativement un certain nombre de structures de données, telles que :

Globalement, les ressources sont multiples et bien présentes, quoique les interfaces soient parfois un peu archaïques et pas forcément très agréables à utiliser…

Dans « Emacs Lisp », il y a « Lisp »…

Et qui dit Lisp, dit macros. Elisp a un mécanisme de macros simple mais tout à fait fonctionnel, qui ressemble un peu à celui de Common Lisp, en moins évolué. La définition se fait avec defmacro, on peut générer des symboles avec make-symbol, et les opérateurs `, ,, et ,@ sont disponibles avec leur sémantique habituelle (quasi-littéral, substitution d’éléments évalués dans un quasi-littéral, et substitution et aplatissement).

Par rapport à des systèmes plus sophistiqués, on peut remarquer, par exemple, que les macros ne peuvent être déclarées qu’avec un seul niveau de paramètres (ceux-ci peuvent bien entendu prendre n’importe quelle valeur) ; la structure interne des arguments doit être décortiquée à la main dans le code de la macro.

En pratique, les macros en Elisp sont largement utilisées ; on en voit un peu partout, tous les jours. Cependant, un détail à garder en tête lorsque l’on emploie des macros complexes : Elisp, contrairement à la plupart des implémentations de Common Lisp, ne compile pas ses fonctions au fur et à mesure, et par là j’entends que le code n’est même pas traduit en bytecode. Cela signifie que les macros seront réévaluées à chaque exécution de la fonction… une des raisons pour lesquelles la plupart des gros paquetages Emacs vous demandent de compiler avant usage. À titre d’exemple, l’utilisation des macros fournies par cl-macs ou pcase (dont nous reparlerons un peu plus loin) sont de bons indicateurs pour identifier les candidats à la compilation obligatoire…

Et le fonctionnel dans tout ça ? Et la POO ?

Après ce petit tour des fondamentaux du langage, l’heure est venue de poser la question qui fâche : c’est bien joli, tout ça, mais quels possibilités Emacs offre-t-il en termes de programmation pour les hommes (et les femmes), les vrais, qui mangent du fonctionnel au petit déjeuner (et des objets au goûter) ?

Fonctions anonymes et fonctions d’ordre supérieur

Commençons par les choses triviales : Elisp dispose de fonctions anonymes (avec lambda) et de valeurs-fonctions de première classe, ce qui rend possible les fonctions d’ordre supérieur — telles que mapcar — dont l’usage est cependant peut-être un peu moins populaire que dans d’autres dialectes de Lisp, du fait du manque de variété de tels opérateurs présents par défaut.

Avec Emacs 24 et la portée lexicale, les fermetures sont maintenant également lexicales lorsque lexical-binding est actif ; les variables globales (dites spéciales) demeurent dynamiques quoi qu’il en soit.

Comme Elisp est un Lisp-2, on ne peut pas tout simplement appeler une fermeture contenue dans une variable par son nom ; il faut utiliser funcall ou apply. Elisp offre plusieurs possibilités de traitement des arguments (par le biais des mots-clés &optional et &rest, ainsi que &key avec les extensions Common Lisp), mais la curryfication n’en fait pas partie ; on notera toutefois la présence de la fonction apply-partially qui, exactement comme son nom l’indique, retourne le résultat de l’application partielle d’une fonction à une liste (éventuellement incomplète) d’arguments.

Oh, les jolis motifs…

Les fonctions anonymes, ça ne fait pas tout ; s’il y a un truc que les fonctionnelleux comme gasche aiment bien, c’est le pattern matching, ou le filtrage par motifs, en français. C’est traditionnellement le domaine des langages de la famille ML, qui ont des types algébriques, et pas franchement le fort d’Elisp, qui a un typage dynamique assez primitif. Malgré cela, Emacs 24 introduit une petite nouveauté, sous la forme de la macro pcase. Celle-ci permet un filtrage par motifs sur les listes et les constantes, prend en charge les gardes booléennes et les affectations de variables ; que demande le peuple ? Illustration :

(pcase '(foo (0 1 2) 3)
  (`(foo ,(and `(0 1 2) xs) ,x)
   (cons x xs)))
     => (3 0 1 2)

Deux détails qui peuvent porter à confusion au départ :

Avec pcase, on peut imaginer coder simplement un semblant d’union typée annotée avec des listes et des symboles :

;; Un arbre binaire...
(defun leaf (x) `(leaf ,x))
(defun node (left right) `(node ,left ,right))

(defun height (tree)
  (pcase tree
    (`(leaf ,_) 0)
    (`(node ,left ,right)
     (1+ (max (height left) (height right))))))

(height (node (leaf 1)
              (node (node (leaf 2)
                          (leaf 3))
                    (leaf 4))))
     => 3

Bien sûr, on pourrait se coder une petite macro pour rendre la définition de telles unions un peu moins vilaine (mais là n’est pas vraiment l’objectif de ce billet) :

(defmacro defunion (&rest alternatives)
  `(progn
     ,@(mapcar (lambda (alt)
                 `(defun ,(car alt) ,(cdr alt)
                    (list ',(car alt) ,@(cdr alt))))
               alternatives)))

(defunion
  (leaf x)
  (node left right))

Des objets, en veux-tu ? EIEIO

Pour finir sur une note rigolote, pour ceux qui souhaitent se faire du mal, Elisp dispose depuis la version 23.2 d’EIEIO, sa propre couche de compatibilité avec CLOS, le système d’objets de Common Lisp. Je n’ai jamais touché moi-même, mais il paraît que c’est assez complet. On y retrouve defclass, defgeneric et compagnie. De quoi satisfaire les envies les plus folles…

Trop chouette, je veux m’y mettre !

Si après cette petite visite guidée, vous êtes encore devant votre écran, n’avez pas régurgité votre quatre-heures, ou mieux, êtes pris d’une envie irrésistible de vous essayer à ce langage, ce ne sont pas vraiment les ressources qui manquent ; il y a un manuel Elisp (consultable avec info, C-h i). EmacsWiki a également des ressources dont l’Elisp Cookbook, pour quelques recettes de base. Alternativement, vous pouvez tout simplement vous lancer dans l’aventure en lisant et modifiant votre extension préférée ; quelque part, c’est joindre l’utile à l’apprentissage (agréable ou pas, selon les personnes). Restez raisonnable — cc-mode n’est probablement pas le meilleur choix pour commencer — et tout ira bien ! Et bienvenue dans le monde merveilleux d’Emacs !