Dlaczego określanie zakresu defvar działa inaczej bez wartości początkowej?

10

Załóżmy, że mam plik o nazwie 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

Ładuję ten plik, a następnie przechodzę do bufora scratch i uruchamiam:

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

(f1 5)zwraca 5 zgodnie z oczekiwaniami, co wskazuje, że treść f1traktowana jest my-dynamic-varjak zmienna o dynamicznym zasięgu, zgodnie z oczekiwaniami. Jednak ostatnia forma podaje błąd zmiennej void dla my-dynamic-var, wskazując, że używa ona leksykalnego zakresu dla tej zmiennej. Wydaje się to sprzeczne z dokumentacją defvar, która mówi:

defvarForma deklaruje również zmienną jako „Special”, tak, że zawsze jest związany dynamicznie nawet jeśli lexical-bindingjest t.

Jeśli zmienię defvarformularz w pliku testowym, aby podać wartość początkową, zmienna jest zawsze traktowana jako dynamiczna, jak głosi dokumentacja. Czy ktoś może wyjaśnić, dlaczego zakres zmiennej zależy od tego, czy defvarpodczas deklarowania tej zmiennej podano wartość początkową?

Oto ślad śledzenia błędu, na wypadek, gdyby miał on znaczenie:

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
źródło
4
Myślę, że dyskusja w błędzie # 18059 jest istotna.
Basil
Świetne pytanie i tak, proszę zobaczyć dyskusję o błędzie # 18059.
Drew
Rozumiem, więc wygląda na to, że dokumentacja zostanie zaktualizowana, aby rozwiązać ten problem w Emacs 26.
Ryan C. Thompson

Odpowiedzi:

8

To, dlaczego oboje są traktowani inaczej, jest głównie „ponieważ tego potrzebowaliśmy”. Mówiąc dokładniej, forma jednoparlamentarna defvarpojawiła się dawno temu, ale później niż druga i była w zasadzie „hackem” do wyciszenia ostrzeżeń kompilatora: w czasie wykonywania nie miała żadnego efektu, więc jako „wypadek” oznaczała że zachowanie wyciszania (defvar FOO)dotyczyło tylko bieżącego pliku (ponieważ kompilator nie mógł wiedzieć, że taki defvar został wykonany w innym pliku).

Kiedy lexical-bindingzostał wprowadzony w Emacs-24, zdecydowaliśmy się ponownie wykorzystać(defvar FOO)formę, ale to oznacza, że teraz nie mają wpływu.

Częściowo w celu zachowania poprzedniego zachowania „wpływa tylko na bieżący plik”, ale co ważniejsze, aby biblioteka mogła używać totojako var dynamicznie skalowanego, nie uniemożliwiając innym bibliotekom używania totojako var zmiennego leksykalnie (zazwyczaj konwencja nazewnictwa prefiksów pakietu unika takich konflikty, ale niestety nie wszędzie się to stosuje), nowe zachowanie (defvar FOO)zdefiniowano tak, aby dotyczyło tylko bieżącego pliku, a nawet udoskonalono, więc dotyczy tylko bieżącego zakresu (np. jeśli pojawia się w funkcji, wpływa tylko na użycie które różnią się w ramach tej funkcji).

Zasadniczo (defvar FOO VAL)i (defvar FOO)są to tylko dwie „zupełnie różne” rzeczy. Zdarza się, że używają tego samego słowa kluczowego ze względów historycznych.

Stefan
źródło
1
+1 za odpowiedź. Ale podejście Common Lisp jest jaśniejsze i lepsze, IMHO.
Drew
@Drew: W większości się zgadzam, ale ponowne użycie (defvar FOO)sprawia, że ​​nowy tryb jest znacznie bardziej kompatybilny ze starym kodem. Ponadto, IIRC stanowi problem z rozwiązaniem CommonLisp, ponieważ jest dość kosztowny dla takiego interpretera jak Elisp (np. Za każdym razem, gdy letoceniasz, musisz zajrzeć do jego ciała, na wypadek, gdyby miało to declarewpływ na niektóre zmienne).
Stefan
Uzgodnione w obu przypadkach.
Drew
4

Na podstawie eksperymentów uważam, że problem polega na tym, że (defvar VAR)brak wartości początkowej ma wpływ tylko na biblioteki, w których się pojawia.

Po dodaniu (defvar my-dynamic-var)do *scratch*bufora błąd już nie występował.

Początkowo myślałem, że było to spowodowane oceną tego formularza, ale potem zauważyłem po pierwsze, że wystarczyło przejrzenie pliku z obecnym formularzem; a ponadto samo dodanie (lub usunięcie) tej formy w buforze, bez oceny, wystarczyło zmienić to, co się stało podczas oceny (let ((my-dynamic-var 5)) (f2))w tym samym buforze za pomocą eval-last-sexp.

(Nie do końca rozumiem, co się tutaj dzieje. Uważam to zachowanie za zaskakujące, ale nie jestem zaznajomiony ze szczegółami implementacji tej funkcji.)

Dodam, że ta forma defvar(bez wartości początkowej) zapobiega narzekaniu przez kompilator bajtów na użycie zewnętrznej zmiennej dynamicznej w kompilowanym pliku elisp, ale sama w sobie nie powoduje, że ta zmienna jest boundp; więc nie jest to ścisłe definiowanie zmiennej. (Zauważ, że gdyby zmienna była, boundp to problem w ogóle nie wystąpiłby).

W praktyce przypuszczam to będzie działać na OK pod warunkiem, że nie zawierają (defvar my-dynamic-var)w każdej bibliotece leksykalnym wiążącego, który wykorzystuje swoją my-dynamic-varzmienną (która przypuszczalnie będzie miało prawdziwą definicję gdzie indziej).


Edytować:

Dzięki wskaźnikowi z @npostavs w komentarzach:

Zarówno eval-last-sexpi eval-defunużywać eval-sexp-add-defvarsw celu:

Dodaj EXP ze wszystkimi literami defvarpoprzedzającymi go w buforze.

Konkretnie to lokalizuje wszystkie defvar, defconsti defcustominstancje. (Nawet gdy jestem komentowany, zauważam.)

Ponieważ przeszukuje bufor w czasie połączenia, wyjaśnia, w jaki sposób te formularze mogą mieć wpływ na bufor nawet bez oceny, i potwierdza, że ​​formularz musi pojawić się w tym samym pliku elisp (a także wcześniej niż kod poddawany ocenie) .

phils
źródło
2
IIUC, błąd nr 18059 potwierdza twoje ćwiczenia.
Basil
2
Wygląda na to, że eval-sexp-add-defvarssprawdza defvary w tekście bufora.
npostavs
1
+1. Oczywiście ta funkcja nie jest jasna lub nie jest jasno przedstawiona użytkownikom. Pomocna jest poprawka doc dla błędu 18059, ale dla użytkowników jest to nadal coś tajemniczego, jeśli nie delikatnego.
Drew
0

Nie mogę tego w ogóle odtworzyć, ocena tego ostatniego fragmentu działa tutaj dobrze i zwraca 5 zgodnie z oczekiwaniami. Czy na pewno nie oceniasz my-dynamic-varsamodzielnie? Spowoduje to błąd, ponieważ zmienna jest nieważna, nie została ustawiona na wartość i będzie miała tylko jedną, jeśli dynamicznie powiążesz ją z jedną.

wasamasa
źródło
1
Czy ustawiłeś wartość lexical-bindinginną niż zero przed oceną formularzy? Otrzymuję zachowanie, które lexical-bindingopisujesz jako zero, ale kiedy ustawię to na zero, pojawia się błąd zmiennej void.
Ryan C. Thompson
Tak, zapisałem to w osobnym pliku, cofnąłem, sprawdziłem, czy lexical-bindingjest ustawiony, i przeanalizowałem formularze sekwencyjnie.
wasamasa
@wasamasa Reproduces dla mnie, może przypadkowo podałeś my-dynamic-varwartość dynamiczną najwyższego poziomu w bieżącej sesji? Myślę, że to może oznaczać, że jest to trwale wyjątkowe.
npostavs