Pourquoi la portée de defvar fonctionne-t-elle différemment sans une valeur init?

10

Supposons que j'ai un fichier nommé elisp-defvar-test.elcontenant:

;;; elisp-defvar-test.el ---  -*- lexical-binding: t -*- 

(defvar my-dynamic-var)

(defun f1 (x)
  "Should return X."
  (let ((my-dynamic-var x))
    (f2)))

(defun f2 ()
  "Returns the current value of `my-dynamic-var'."
  my-dynamic-var)

(provide 'elisp-dynamic-test)

;;; elisp-defvar-test.el ends here

Je charge ce fichier, puis je vais dans le tampon de travail et je lance:

(setq lexical-binding t)
(f1 5)
(let ((my-dynamic-var 5))
  (f2))

(f1 5)renvoie 5 comme prévu, indiquant que le corps de f1est traité my-dynamic-varcomme une variable de portée dynamique, comme prévu. Cependant, le dernier formulaire donne une erreur de variable nulle pour my-dynamic-var, indiquant qu'il utilise la portée lexicale pour cette variable. Cela semble en contradiction avec la documentation de defvar, qui dit:

Le defvarformulaire déclare également la variable comme "spéciale", de sorte qu'elle est toujours liée dynamiquement même si lexical-bindingest t.

Si je modifie le defvarformulaire dans le fichier de test pour fournir une valeur initiale, la variable est toujours traitée comme dynamique, comme le dit la documentation. Quelqu'un peut-il expliquer pourquoi la portée d'une variable est déterminée par le fait d'avoir defvarreçu ou non une valeur initiale lors de la déclaration de cette variable?

Voici la trace d'erreur, au cas où cela serait important:

Debugger entered--Lisp error: (void-variable my-dynamic-var)
  f2()
  (let ((my-dynamic-var 5)) (f2))
  (progn (let ((my-dynamic-var 5)) (f2)))
  eval((progn (let ((my-dynamic-var 5)) (f2))) t)
  elisp--eval-last-sexp(t)
  eval-last-sexp(t)
  eval-print-last-sexp(nil)
  funcall-interactively(eval-print-last-sexp nil)
  call-interactively(eval-print-last-sexp nil nil)
  command-execute(eval-print-last-sexp)
Ryan C. Thompson
la source
4
Je pense que la discussion dans le bogue # 18059 est pertinente.
Basil
Grande question, et oui, veuillez voir la discussion du bogue # 18059.
Drew
Je vois, il semble donc que la documentation sera mise à jour pour y remédier dans Emacs 26.
Ryan C. Thompson

Réponses:

8

La raison pour laquelle les deux sont traités différemment est principalement «parce que c'est ce dont nous avions besoin». Plus spécifiquement, la forme à un seul argument de defvarest apparue il y a longtemps, mais plus tard que l'autre et était fondamentalement un "hack" pour faire taire les avertissements du compilateur: au moment de l'exécution, cela n'avait aucun effet, donc comme un "accident" cela signifiait que le comportement de silence de (defvar FOO)ne s'appliquait qu'au fichier actuel (puisque le compilateur n'avait aucun moyen de savoir qu'un tel defvar avait été exécuté dans un autre fichier).

Quand lexical-bindinga été introduit dans Emacs-24, nous avons décidé de réutiliser cette (defvar FOO)forme, mais cela implique que maintenant le fait avoir un effet.

En partie pour préserver le comportement précédent "n'affecte que le fichier actuel", mais plus important encore pour permettre à une bibliothèque de l'utiliser totocomme var à portée dynamique sans empêcher d'autres bibliothèques d'utiliser totocomme var à portée lexicale (généralement la convention de dénomination package-prefix évite celles conflits, mais il n'est malheureusement pas utilisé partout), le nouveau comportement de a (defvar FOO)été défini pour s'appliquer uniquement au fichier actuel, et a même été affiné pour ne s'appliquer qu'à la portée actuelle (par exemple, s'il apparaît dans une fonction, il n'affecte que les utilisations de qui var dans cette fonction).

Fondamentalement, (defvar FOO VAL)et ne (defvar FOO)sont que deux choses "complètement différentes". Ils utilisent simplement le même mot-clé pour des raisons historiques.

Stefan
la source
1
+1 pour la réponse. Mais l'approche de Common Lisp est plus claire et meilleure, à mon humble avis.
Drew
@Drew: Je suis généralement d'accord, mais la réutilisation (defvar FOO)rend le nouveau mode beaucoup plus compatible avec l'ancien code. De plus, l'IIRC un problème avec la solution de CommonLisp est qu'elle est assez coûteuse pour un interprète pur comme Elisp (par exemple, chaque fois que vous évaluez un, letvous devez regarder à l'intérieur de son corps au cas où il y en aurait un declarequi affecterait certains des vars).
Stefan
D'accord sur les deux points.
Drew
4

Sur la base de l'expérimentation, je pense que le problème est que, (defvar VAR)sans valeur init, cela n'a d'effet que sur la ou les bibliothèques dans lesquelles il apparaît.

Lorsque j'ai ajouté (defvar my-dynamic-var)au *scratch*tampon, l'erreur ne s'est plus produite.

À l'origine, je pensais que c'était à cause de l' évaluation de ce formulaire, mais j'ai d'abord remarqué que la simple visite du dossier avec ce formulaire était suffisante; et en outre, le simple fait d'ajouter (ou de supprimer) cette forme dans le tampon, sans l'évaluer, a suffi pour changer ce qui s'est passé lors de l'évaluation à l' (let ((my-dynamic-var 5)) (f2))intérieur de ce même tampon avec eval-last-sexp.

(Je ne comprends pas vraiment ce qui se passe ici. Je trouve le comportement surprenant, mais je ne connais pas les détails de la façon dont cette fonctionnalité est mise en œuvre.)

J'ajouterai que cette forme de defvar(sans valeur init) empêche le compilateur d'octets de se plaindre des utilisations d'une variable dynamique définie en externe dans le fichier elisp en cours de compilation, mais en soi, cela ne fait pas que cette variable soit boundp; il ne s'agit donc pas de définir strictement la variable. (Notez que si la variable était boundp alors ce problème ne se produirait pas du tout.)

Dans la pratique , je suppose que cela va fonctionner ok à condition que vous n'inclure dans une bibliothèque lexicales de liaison qui utilise votre variable (qui aurait probablement une définition réelle ailleurs).(defvar my-dynamic-var)my-dynamic-var


Éditer:

Merci au pointeur de @npostavs dans les commentaires:

Les deux eval-last-sexpet les eval-defunutiliser eval-sexp-add-defvarspour:

Ajoutez EXP à tous les defvars qui le précèdent dans le tampon.

Plus précisément , il localise tous defvar, defconstet les defcustominstances. (Même en commentant, je le remarque.)

Comme il recherche le tampon au moment de l'appel, il explique comment ces formulaires peuvent avoir un effet dans le tampon même sans être évalué, et confirme que le formulaire doit apparaître dans le même fichier elisp (et aussi plus tôt que le code évalué) .

phils
la source
2
IIUC, le bogue # 18059 confirme vos expériences.
Basil
2
Semble que eval-sexp-add-defvarsvérifie les defvars dans le texte du tampon.
npostavs le
1
+1. De toute évidence, cette fonctionnalité n'est pas claire ou n'est pas clairement présentée aux utilisateurs. Le correctif doc pour le bogue # 18059 aide, mais c'est toujours quelque chose de mystérieux, sinon fragile, pour les utilisateurs.
Drew
0

Je ne peux pas reproduire cela du tout, l'évaluation de ce dernier extrait fonctionne très bien ici et renvoie 5 comme prévu. Êtes-vous sûr de ne pas évaluer my-dynamic-varseul? Cela générera une erreur car la variable est nulle, elle n'a pas été définie sur une valeur et elle n'en aura qu'une si vous la liez dynamiquement à une.

wasamasa
la source
1
Avez-vous défini lexical-bindingnon nul avant d'évaluer les formulaires? J'obtiens le comportement que vous décrivez avec lexical-bindingnil, mais quand je le mets à non-nil, j'obtiens l'erreur void-variable.
Ryan C. Thompson
Oui, je l'ai enregistré dans un fichier séparé, rétabli, vérifié qui lexical-bindingest défini et évalué les formulaires de manière séquentielle.
wasamasa
@wasamasa Reproduit pour moi, peut-être avez-vous accidentellement donné my-dynamic-varune valeur dynamique de haut niveau dans votre session actuelle? Je pense que cela pourrait le marquer de façon permanente.
npostavs le