Comment vivre avec la scope dynamic d’Emacs Lisp?

J’ai déjà appris la Clojure et j’aime vraiment la langue. J’aime aussi Emacs et j’ai piraté des choses simples avec Emacs Lisp. Cependant, il y a une chose qui m’empêche de faire quelque chose de plus substantiel avec Elisp. C’est le concept de cadrage dynamic. Je suis juste effrayé car il est si étranger à moi et sent les variables semi-globales.

Donc, avec les déclarations variables, je ne sais pas quelles sont les choses à faire et lesquelles sont dangereuses. D’après ce que j’ai compris, les variables définies avec setq relèvent de la scope dynamic (est-ce exact?) Qu’en est-il des variables laissées? Quelque part, j’ai lu que laisser vous permet de faire de la scope lexicale simple, mais quelque part, je lis que laisser les vars sont également de manière dynamic.

Je pense que mon plus grand souci est que mon code (en utilisant setq ou let) casse accidentellement certaines variables du code de la plate-forme ou du tiers que j’appelle ou que, après un tel appel, mes variables locales soient accidentellement endommagées. Comment puis-je éviter ça?

Existe-t-il quelques règles simples que je peux simplement suivre et savoir exactement ce qui se passe avec le scope sans être mordu par un moyen étrange et difficile à déboguer?

Ce n’est pas si mal

Les principaux problèmes peuvent apparaître avec des «variables libres» dans les fonctions.

(defun foo (a) (* ab)) 

Dans la fonction ci-dessus, a est une variable locale. b est une variable libre. Dans un système avec une liaison dynamic comme Emacs Lisp, b sera recherché à l’exécution. Il y a maintenant trois cas:

  1. b n’est pas défini -> erreur
  2. b est une variable locale liée par un appel de fonction dans la scope dynamic actuelle -> prend cette valeur
  3. b est une variable globale -> prend cette valeur

Les problèmes peuvent alors être:

  • une valeur liée (globale ou locale) est masquée par un appel de fonction, éventuellement indésirable
  • une variable non définie n’est PAS ombrée -> erreur d’access
  • une variable globale n’est PAS ombrée -> prend la valeur globale, qui peut être indésirable

Dans un Lisp avec un compilateur, la compilation de la fonction ci-dessus peut générer un avertissement indiquant qu’il existe une variable libre. Les compilateurs Lisp commun le feront généralement. Un interprète ne fournira pas cet avertissement, on verra juste l’effet au moment de l’exécution.

Conseil :

  • assurez-vous de ne pas utiliser accidentellement des variables libres
  • assurez-vous que les variables globales ont un nom spécial, de sorte qu’elles soient faciles à repérer dans le code source, généralement *foo-var*

N’écris pas

 (defun foo (ab) ... (setq c (* ab)) ; where c is a free variable ...) 

Écrire:

 (defun foo (ab) ... (let ((c (* ab))) ...) ...) 

Liez toutes les variables que vous voulez utiliser et assurez-vous qu’elles ne sont pas liées ailleurs.

C’est fondamentalement ça.

Depuis la version lexicale de GNU Emacs version 24 est prise en charge dans son Lisp Emacs. Voir: Liaison Lexical, Manuel de référence GNU Emacs Lisp .

Existe-t-il quelques règles simples que je peux simplement suivre et savoir exactement ce qui se passe avec le scope sans être mordu par un moyen étrange et difficile à déboguer?

Lisez Emacs Lisp Reference , vous aurez beaucoup de détails comme celui-ci:

  • Formulaire spécial: setq [forme de symbole] … Cette forme spéciale est la méthode la plus courante pour modifier la valeur d’une variable. Chaque SYMBOL reçoit une nouvelle valeur, qui résulte de l’évaluation du FORM correspondant. La liaison existante la plus locale du symbole est modifiée .

Voici un exemple :

 (defun foo () (setq tata "foo")) (defun bar (tata) (setq tata "bar")) (foo) (message tata) ===> "foo" (bar tata) (message tata) ===> "foo" 

En plus du dernier paragraphe de la réponse de Gilles, voici comment RMS plaide en faveur de la scope dynamic dans un système extensible:

Certains concepteurs de langage estiment que la liaison dynamic doit être évitée et que le passage explicite d’arguments doit être utilisé à la place. Imaginez que la fonction A lie la variable FOO et appelle la fonction B, qui appelle la fonction C, et C utilise la valeur de FOO. Soi-disant A devrait passer la valeur comme argument à B, qui devrait le passer en argument à C.

Cela ne peut toutefois pas être fait dans un système extensible, car l’auteur du système ne peut pas savoir quels seront tous les parameters. Imaginez que les fonctions A et C font partie d’une extension utilisateur, tandis que B fait partie du système standard. La variable FOO n’existe pas dans le système standard; cela fait partie de l’extension. Pour utiliser un argument explicite, il faudrait append un nouvel argument à B, ce qui signifie réécrire B et tout ce qui appelle B. Dans le cas le plus courant, B est la boucle du répartiteur de commandes de l’éditeur.

Ce qui est pire, C doit également recevoir un argument supplémentaire. B ne se réfère pas à C par son nom (C n’existait pas quand B était écrit). Il trouve probablement un pointeur sur C dans la table de dissortingbution des commandes. Cela signifie que le même appel qui appelle parfois C peut également appeler n’importe quelle définition de commande de l’éditeur. Toutes les commandes d’édition doivent donc être réécrites pour accepter et ignorer l’argument supplémentaire. À ce jour, il ne rest plus rien du système d’origine!

Personnellement, je pense que s’il y a un problème avec Emacs-Lisp, ce n’est pas la scope dynamic en soi, mais c’est la valeur par défaut, et il n’est pas possible d’obtenir une scope lexicale sans recourir à des extensions. Dans CL, la scope dynamic et lexicale peuvent être utilisées, et, à l’exception du niveau supérieur (auquel s’adressent plusieurs implémentations par défaut) et des variables spéciales déclarées globalement, la définition par défaut est la scope lexicale. Dans Clojure, vous pouvez également utiliser la scope lexicale et dynamic.

Pour citer à nouveau RMS:

Il n’est pas nécessaire que la scope dynamic soit la seule règle de scope fournie, mais utile pour qu’elle soit disponible.

Comme l’a souligné Peter Ajtai:

Depuis emacs-24.1, vous pouvez activer la scope lexicale par fichier en plaçant

 ;; -*- lexical-binding: t -*- 

en haut de votre fichier elisp.

Premièrement, elisp a des liaisons de variables et de fonctions distinctes, de sorte que certains pièges de la scope dynamic ne sont pas pertinents.

Deuxièmement, vous pouvez toujours utiliser setq pour définir des variables, mais le jeu de valeurs ne survit pas à la sortie de la scope dynamic dans laquelle il se trouve. Ce n’est pas fondamentalement différent de la scope lexicale, à la différence de la définition dynamic Dans une fonction, vous appelez peut affecter la valeur que vous voyez après l’appel de fonction.

Il y a lexical-let , une macro qui imite (essentiellement) les liaisons lexicales (je crois que cela se fait en parcourant le corps et en changeant toutes les occurrences des variables lexicales en un nom générique, éventuellement en supprimant le symbole).

Je dirais “écrire le code normalement”. Il y a des moments où la nature dynamic de l’elisp va vous mordre, mais j’ai trouvé que, dans la pratique, c’est étonnamment rare.

Voici un exemple de ce que je disais à propos de setq et des variables liées dynamicment (récemment évaluées dans un tampon de travail à proximité):

 (let ((a nil)) (list (let ((a nil)) (setq a 'value) a) a)) (value nil) 

Tout ce qui a été écrit ici vaut la peine. J’appendais ceci: apprenez à connaître Common Lisp – si rien d’autre, lisez-le. CLTL2 présente bien une liaison lexicale et dynamic, comme d’autres livres. Et Common Lisp les intègre bien dans une seule langue.

Si vous le “comprenez” après une exposition à Common Lisp, alors les choses seront plus claires pour Emacs Lisp. Emacs 24 utilise la scope lexicale par défaut davantage que les anciennes versions, mais l’approche de Lisp commun sera toujours plus claire et plus propre (IMHO). Enfin, il est certain que la scope dynamic est importante pour Emacs Lisp, pour les raisons que RMS et d’autres ont mis en avant.

Donc, ma suggestion est de savoir comment Common Lisp traite avec cela. Essayez d’oublier Scheme, si c’est votre modèle mental principal de Lisp – il vous limitera plus que vous pour mieux comprendre la scope, les funargs, etc. dans Emacs Lisp. Emacs Lisp, comme Common Lisp, est “sale et faible”; ce n’est pas Scheme.

La scope dynamic et lexicale a des comportements différents lorsqu’un morceau de code est utilisé dans une scope différente de celle dans laquelle il a été défini. En pratique, il existe deux modèles qui couvrent la plupart des cas problématiques:

  • Une fonction masque une variable globale, puis appelle une autre fonction qui utilise cette variable globale.

     (defvar x 3) (defun foo () x) (defun bar (x) (+ (foo) x)) (bar 0) ⇒ 0 

    Cela ne se produit pas souvent dans Emacs parce que les variables locales ont tendance à avoir des noms courts (souvent un seul mot) alors que les variables globales ont tendance à avoir des noms longs (souvent préfixés par le nom du packagename- ). De nombreuses fonctions standard ont des noms qui sont tentants d’utiliser des variables locales comme les list et les point , mais les fonctions et les variables qui vivent dans des espaces de noms distincts sont des fonctions locales peu utilisées.

  • Une fonction est définie dans un contexte lexical et utilisée en dehors de ce contexte lexical, car elle est transmise à une fonction d’ordre supérieur.

     (let ((cl-y 10)) (mapcar* (lambda (elt) (* cl-y elt)) '(1 2 3))) ⇒ (10 20 30) (let ((cl-x 10)) (mapcar* (lambda (elt) (* cl-x elt)) '(1 2 3))) ⇑ (wrong-type-argument number-or-marker-p (1 2 3)) 

    L’erreur est due à l’utilisation de cl-x comme nom de variable dans mapcar* (à partir du package cl ). Notez que le package cl utilise cl- comme préfixe même pour ses variables locales dans les fonctions d’ordre supérieur. Cela fonctionne assez bien dans la pratique, du moment que vous prenez soin de ne pas utiliser la même variable en tant que nom global et nom local, et que vous n’avez pas besoin d’écrire une fonction récursive d’ordre supérieur.

L’âge de PS Emacs Lisp n’est pas la seule raison pour laquelle il est dynamic. Certes, à cette époque, les lisps tendaient vers la scope dynamic – Scheme et Common Lisp n’avaient pas encore vraiment pris. Mais le cadrage dynamic est également un atout dans un langage visant à étendre dynamicment un système: il vous permet de vous connecter à plus d’endroits sans aucun effort particulier. Avec une grande puissance, il y a une grande corde à suspendre: vous risquez de vous accrocher accidentellement à un endroit inconnu.

Les autres réponses expliquent bien les détails techniques sur la manière de travailler avec la scope dynamic. Voici donc mon conseil non technique:

Fais-le

Je bricole avec Emacs Lisp depuis plus de 15 ans et je ne sais pas si j’ai déjà été mordu par des problèmes dus aux différences entre le champ lexical / dynamic.

Personnellement, je n’ai pas trouvé le besoin de fermetures (je les aime, mais n’en ai pas besoin pour Emacs). Et, en général, j’essaie d’éviter les variables globales en général (que la scope soit lexicale ou dynamic).

Donc, je suggère de sauter et d’écrire des personnalisations qui répondent à vos besoins / désirs, il y a des chances que vous n’ayez aucun problème.

Je ressens entièrement ta douleur. Je trouve le manque de liaison lexicale dans emacs plutôt ennuyeux – surtout de ne pas pouvoir utiliser les fermetures lexicales, ce qui semble être une solution que je pense beaucoup, venant de langues plus modernes.

Bien que je ne dispose pas de plus de conseils sur les fonctionnalités manquantes que les réponses précédentes ne couvraient pas encore, je voudrais souligner l’existence d’une twig emacs appelée «lexbind», qui implémente une liaison lexicale dans une manière compatible. Dans mon expérience, les fermetures lexicales sont encore un peu déroutantes dans certaines circonstances, mais cette twig semble être une approche prometteuse.

Juste pas.

Emacs-24 vous permet d’utiliser la scope lexicale. Juste courir

(setq lexical-binding t)

ou append

;; -*- lexical-binding: t -*-

au début de votre fichier.