Warum funktioniert das Defvar-Scoping ohne Initialwert anders?

10

Angenommen, ich habe eine Datei mit dem Namen elisp-defvar-test.el:

;;; 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

Ich lade diese Datei und gehe dann in den Arbeitspuffer und starte:

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

(f1 5)Gibt wie erwartet 5 zurück, was darauf hinweist, dass der Body von wie erwartet als Variable mit dynamischem Gültigkeitsbereich f1behandelt wird my-dynamic-var. Das letzte Formular gibt jedoch einen Void-Variablenfehler für an my-dynamic-var, der angibt, dass für diese Variable ein lexikalischer Gültigkeitsbereich verwendet wird. Dies scheint im Widerspruch zu der Dokumentation zu stehen defvar, die besagt:

Das defvarFormular deklariert die Variable auch als "speziell", so dass sie immer dynamisch gebunden ist, auch wenn lexical-bindingt ist.

Wenn ich das defvarFormular in der Testdatei so ändere , dass ein Anfangswert angegeben wird, wird die Variable immer als dynamisch behandelt, wie in der Dokumentation angegeben. Kann jemand erklären, warum der Umfang einer Variablen davon abhängt, ob defvarbei der Deklaration dieser Variablen ein Anfangswert angegeben wurde oder nicht ?

Hier ist die Fehlerrückverfolgung, falls es darauf ankommt:

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
quelle
4
Ich denke, die Diskussion in Fehler # 18059 ist relevant.
Basil
Gute Frage, und ja, bitte sehen Sie sich die Diskussion über Fehler # 18059 an.
Drew
Ich sehe, also sieht es so aus, als würde die Dokumentation aktualisiert, um dies in Emacs 26 zu beheben.
Ryan C. Thompson

Antworten:

8

Warum die beiden unterschiedlich behandelt werden, ist meistens "weil wir das brauchten". Genauer gesagt, die Einzelargumentform von defvarerschien vor langer Zeit, aber später als die andere und war im Grunde ein "Hack", um Compiler-Warnungen zum Schweigen zu bringen: Zur Ausführungszeit hatte sie überhaupt keine Wirkung, was als "Unfall" bedeutete dass das Stummschaltungsverhalten (defvar FOO)nur auf die aktuelle Datei angewendet wurde (da der Compiler nicht wissen konnte, dass eine solche Defvar in einer anderen Datei ausgeführt wurde).

Wenn lexical-bindingin Emacs-24 eingeführt wurde, haben wir beschlossen, wieder verwenden diese (defvar FOO)Form, aber das bedeutet , dass es jetzt tut eine Wirkung haben.

Teilweise, um das vorherige Verhalten "wirkt sich nur auf die aktuelle Datei aus" totobeizubehalten , aber noch wichtiger, um zu ermöglichen, dass eine Bibliothek als Variable mit dynamischem Gültigkeitsbereich verwendet wird, ohne zu verhindern, dass andere Bibliotheken totoals Variable mit lexikalischem Gültigkeitsbereich verwendet werden (normalerweise werden diese durch die Namenskonvention für Paketpräfixe vermieden Konflikte, aber es wird leider nicht überall verwendet), das neue Verhalten von (defvar FOO)wurde so definiert, dass es nur für die aktuelle Datei gilt, und wurde sogar verfeinert, sodass es nur für den aktuellen Bereich gilt (z. B. wenn es innerhalb einer Funktion angezeigt wird, wirkt es sich nur auf die Verwendung von aus diese var innerhalb dieser Funktion).

Grundsätzlich (defvar FOO VAL)und (defvar FOO)sind nur zwei "völlig verschiedene" Dinge. Sie verwenden aus historischen Gründen zufällig dasselbe Schlüsselwort.

Stefan
quelle
1
+1 für die Antwort. Aber der Ansatz von Common Lisp ist meiner Meinung nach klarer und besser.
Drew
@Drew: Ich stimme größtenteils zu, aber die Wiederverwendung (defvar FOO)macht den neuen Modus viel kompatibler mit altem Code. Ein IIRC-Problem mit der CommonLisp-Lösung besteht auch darin, dass es für einen reinen Interpreter wie den von Elisp ziemlich kostspielig ist (z. B. muss man jedes Mal, wenn man einen bewertet let, in seinen Körper schauen, falls es einen gibt declare, der einige der Vars betrifft).
Stefan
In beiden Punkten einverstanden.
Drew
4

Aufgrund von Experimenten glaube ich, dass das Problem darin besteht, dass ein (defvar VAR)Initialisierungswert nur Auswirkungen auf die Bibliothek (en) hat, in denen er angezeigt wird.

Beim Hinzufügen (defvar my-dynamic-var)zum *scratch*Puffer ist der Fehler nicht mehr aufgetreten.

Ich dachte ursprünglich, dies sei auf die Auswertung dieses Formulars zurückzuführen, bemerkte dann aber zunächst, dass es ausreicht , nur die Datei mit diesem Formular zu besuchen . und außerdem reichte es aus , nur dieses Formular im Puffer hinzuzufügen (oder zu entfernen), ohne es auszuwerten, um zu ändern, was bei der Auswertung (let ((my-dynamic-var 5)) (f2))innerhalb desselben Puffers mit passiert ist eval-last-sexp.

(Ich habe kein wirkliches Verständnis dafür, was hier passiert. Ich finde das Verhalten überraschend, aber ich bin nicht mit den Details der Implementierung dieser Funktionalität vertraut.)

Ich werde hinzufügen, dass diese Form von defvar(ohne Init-Wert) den Byte-Compiler daran hindert, sich über die Verwendung einer extern definierten dynamischen Variablen in der zu kompilierenden elisp-Datei zu beschweren, aber allein diese Variable nicht verursacht boundp; Daher wird die Variable nicht streng definiert. (Beachten Sie, dass , wenn die Variable ist boundp dann dieses Problem nicht auftreten würde.)

In der Praxis Ich nehme an, dies zur Verfügung gestellt arbeiten ok wird , dass Sie es sind (defvar my-dynamic-var)in jedem lexikalisch-Bindungsbibliothek , die Ihr verwendet my-dynamic-varVariable (die vermutlich eine wirkliche Definition an anderer Stelle hätte).


Bearbeiten:

Danke an den Zeiger von @npostavs in den Kommentaren:

Beides eval-last-sexpund eval-defunverwenden eval-sexp-add-defvars, um:

Stellen Sie EXP mit allen vorangestellten defvars im Puffer voran .

Insbesondere sucht er alle defvar, defconstund defcustomInstanzen. (Auch wenn ich auskommentiert bin, merke ich es.)

Da hierdurch der Puffer zur Aufrufzeit durchsucht wird, wird erläutert, wie sich diese Formulare auch ohne Auswertung auf den Puffer auswirken können, und es wird bestätigt, dass das Formular in derselben Elisp-Datei (und auch früher als der auszuwertende Code) erscheinen muss. .

Phils
quelle
2
IIUC, Fehler # 18059 bestätigt Ihre Experimente.
Basil
2
Scheint, dass eval-sexp-add-defvarsim Puffertext nach Defvars gesucht wird.
Npostavs
1
+1. Diese Funktion ist eindeutig nicht klar oder wird den Benutzern nicht klar präsentiert. Die Dokumentkorrektur für Fehler Nr. 18059 hilft, aber dies ist für Benutzer immer noch etwas Geheimnisvolles, wenn nicht sogar Fragiles.
Drew
0

Ich kann das überhaupt nicht reproduzieren. Die Auswertung des letzteren Snippets funktioniert hier einwandfrei und gibt erwartungsgemäß 5 zurück. Sind Sie sicher, dass Sie nicht my-dynamic-varalleine bewerten ? Dies führt zu einem Fehler, da die Variable ungültig ist, nicht auf einen Wert festgelegt wurde und nur dann einen Wert hat, wenn Sie sie dynamisch an einen gebunden haben.

Wasamasa
quelle
1
Haben Sie lexical-bindingvor der Auswertung der Formulare Nicht-Null gesetzt? Ich erhalte das Verhalten, das Sie mit lexical-bindingnil beschreiben , aber wenn ich es auf non-nil setze, erhalte ich den Fehler void-variable.
Ryan C. Thompson
Ja, ich habe dies in einer separaten Datei gespeichert, zurückgesetzt, überprüft, dass lexical-bindingdie Formulare nacheinander festgelegt und ausgewertet wurden.
Wasamasa
@wasamasa Reproduziert für mich, vielleicht haben Sie my-dynamic-varin Ihrer aktuellen Sitzung versehentlich einen dynamischen Wert auf höchster Ebene angegeben? Ich denke, das könnte es dauerhaft besonders markieren.
npostavs