Perché lo scoping defvar funziona in modo diverso senza un valore iniziale?


10

Supponiamo di avere un file chiamato elisp-defvar-test.elcontenente:

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

Carico questo file e poi vado nel buffer di memoria ed eseguo:

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

(f1 5)restituisce 5 come previsto, indicando che il corpo di f1sta trattando my-dynamic-varcome una variabile con ambito dinamico, come previsto. Tuttavia, l'ultimo modulo fornisce un errore variabile vuoto per my-dynamic-var, indicando che sta usando l'ambito lessicale per questa variabile. Questo sembra in contrasto con la documentazione per defvar, che dice:

Il defvarmodulo dichiara inoltre la variabile come "speciale", in modo che sia sempre legata dinamicamente anche se lexical-bindingè t.

Se cambio il defvarmodulo nel file di test per fornire un valore iniziale, la variabile viene sempre trattata come dinamica, come dice la documentazione. Qualcuno può spiegare perché l'ambito di una variabile è determinato dal fatto che sia defvarstato fornito o meno un valore iniziale durante la dichiarazione di quella variabile?

Ecco l'errore backtrace, nel caso in cui sia importante:

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)

4
Penso che la discussione nel bug # 18059 sia pertinente.
Basilio

Ottima domanda, e sì, per favore, vedi la discussione sul bug # 18059.
Estratto il

Vedo, quindi sembra che la documentazione verrà aggiornata per risolvere questo problema in Emacs 26.
Ryan C. Thompson,

Risposte:


8

Il motivo per cui i due sono trattati in modo diverso è principalmente "perché è quello di cui avevamo bisogno". Più specificamente, la forma a argomento singolo è defvarapparsa molto tempo fa, ma più tardi dell'altra ed era sostanzialmente un "trucco" per mettere a tacere gli avvisi del compilatore: in fase di esecuzione non aveva alcun effetto, quindi come "incidente" significava che il comportamento di silenziamento si (defvar FOO)applicava solo al file corrente (dal momento che il compilatore non aveva modo di sapere che tale defvar era stato eseguito in qualche altro file).

Quando lexical-bindingè stato introdotto in Emacs-24, abbiamo deciso di ri-utilizzare questo (defvar FOO)modulo, ma che implica che ora ha un effetto.

In parte per preservare il comportamento precedente "influisce solo sul file corrente", ma soprattutto per consentire a una libreria di utilizzare totocome var con ambito dinamico senza impedire ad altre librerie di utilizzare totocome var con ambito lessicale (di solito la convenzione di denominazione con prefisso pacchetto evita quelle conflitti, ma non è usato ovunque tristemente), il nuovo comportamento di è (defvar FOO)stato definito per applicarsi solo al file corrente ed è stato persino perfezionato, quindi si applica solo all'ambito corrente (ad esempio se appare all'interno di una funzione, influenza solo gli usi di quel var all'interno di quella funzione).

Fondamentalmente, (defvar FOO VAL)e (defvar FOO)sono solo due cose "completamente diverse". Capita semplicemente di usare la stessa parola chiave per motivi storici.


1
+1 per la risposta. Ma l'approccio del Common Lisp è più chiaro e migliore, IMHO.
Estratto il

@Drew: Sono per lo più d'accordo, ma il riutilizzo (defvar FOO)rende la nuova modalità molto più compatibile con il vecchio codice. Inoltre, IIRC un problema con la soluzione di CommonLisp è che è piuttosto costoso per un interprete puro come quello di Elisp (ad esempio, ogni volta che si valuta un letsi deve guardare dentro il suo corpo nel caso ce ne sia uno declareche colpisce alcuni dei vari).
Stefan,

Concordato su entrambi i punti.
Estratto il

4

Sulla base della sperimentazione, credo che il problema sia che (defvar VAR)senza alcun valore di init ha solo un effetto sulle librerie in cui appare.

Quando l'ho aggiunto (defvar my-dynamic-var)al *scratch*buffer, l'errore non si è più verificato.

Inizialmente pensavo che ciò fosse dovuto alla valutazione di quel modulo, ma poi ho notato innanzitutto che era sufficiente visitare il file con quel modulo presente; e inoltre che semplicemente aggiungere (o rimuovere) quel modulo nel buffer, senza valutarlo, è stato sufficiente cambiare ciò che è accaduto durante la valutazione (let ((my-dynamic-var 5)) (f2))all'interno dello stesso buffer con eval-last-sexp.

(Non ho una reale comprensione di ciò che sta accadendo qui. Trovo il comportamento sorprendente, ma non conosco i dettagli di come viene implementata questa funzionalità.)

Aggiungerò che questa forma di defvar(senza alcun valore di init) impedisce al compilatore di byte di lamentarsi dell'uso di una variabile dinamica definita esternamente nel file elisp in fase di compilazione, ma da sola non provoca tale variabile boundp; quindi non sta definendo rigorosamente la variabile. (Si noti che se la variabile fosse boundp allora questo problema non si verificherebbe affatto.)

In pratica suppongo che funzionerà bene, a condizione che tu lo includa (defvar my-dynamic-var)in qualsiasi libreria di legame lessicale che utilizza la tua my-dynamic-varvariabile (che presumibilmente avrebbe una vera definizione altrove).


Modificare:

Grazie al puntatore di @npostavs nei commenti:

Entrambi eval-last-sexpe eval-defunutilizzare eval-sexp-add-defvarsper:

Prepaga EXP con tutte le defvars che lo precedono nel buffer.

In particolare si individua tutti defvar, defconste defcustomle istanze. (Anche quando commentato, noto.)

Dato che questo cerca nel buffer al momento della chiamata, spiega come questi moduli possono avere un effetto nel buffer anche senza essere valutati, e conferma che il modulo deve apparire nello stesso file elisp (e anche prima del codice che viene valutato) .


2
IIUC, bug # 18059 conferma i tuoi sforzi.
Basilio

2
Sembra che eval-sexp-add-defvarscontrolli defvars nel testo del buffer.
npostavs,

1
+1. Chiaramente questa funzione non è chiara o non è chiaramente presentata agli utenti. La correzione del documento per il bug # 18059 aiuta, ma questo è ancora qualcosa di misterioso, se non fragile, per gli utenti.
Estratto il

0

Non riesco a riprodurlo affatto, la valutazione di quest'ultimo frammento funziona bene qui e restituisce 5 come previsto. Sei sicuro di non valutare my-dynamic-varda solo? Ciò genererà un errore perché la variabile è nulla, non è stata impostata su un valore e ne avrà uno solo se lo si associa dinamicamente a uno.


1
Hai impostato lexical-bindingun valore zero prima di valutare i moduli? Ottengo il comportamento che descrivi con lexical-bindingzero, ma quando lo imposto su zero, ottengo l'errore variabile vuoto.
Ryan C. Thompson,

Sì, l'ho salvato in un file separato, ripristinato, verificato che lexical-bindingè stato impostato e valutato i moduli in sequenza.
wasamasa,

@wasamasa Riproduce per me, forse hai accidentalmente dato my-dynamic-varun valore dinamico di massimo livello nella sessione corrente? Penso che potrebbe segnarlo in modo permanente speciale.
npostavs,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.