Mengapa defvar scoping bekerja secara berbeda tanpa initvalue?

10

Misalkan saya memiliki file bernama elisp-defvar-test.elmengandung:

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

Saya memuat file ini dan kemudian pergi ke buffer awal dan menjalankan:

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

(f1 5)mengembalikan 5 seperti yang diharapkan, menunjukkan bahwa tubuh f1memperlakukan my-dynamic-varsebagai variabel cakupan dinamis, seperti yang diharapkan. Namun, formulir terakhir memberikan kesalahan variabel void my-dynamic-var, yang menunjukkan bahwa ia menggunakan pelingkupan leksikal untuk variabel ini. Ini sepertinya bertentangan dengan dokumentasi untuk defvar, yang mengatakan:

The defvarbentuk juga menyatakan variabel sebagai "khusus", sehingga selalu dinamis terikat bahkan jika lexical-bindingadalah t.

Jika saya mengubah defvarformulir dalam file pengujian untuk memberikan nilai awal, maka variabel selalu diperlakukan sebagai dinamis, seperti yang dikatakan dokumentasi. Adakah yang bisa menjelaskan mengapa pelingkupan suatu variabel ditentukan oleh apakah defvardiberi nilai awal atau tidak saat mendeklarasikan variabel itu?

Inilah backtrace kesalahan, jika itu penting:

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
sumber
4
Saya pikir diskusi di bug # 18059 relevan.
Basil
Pertanyaan yang bagus, dan ya, tolong lihat diskusi bug # 18059.
Drew
Begitu, jadi sepertinya dokumentasi akan diperbarui untuk mengatasi ini di Emacs 26.
Ryan C. Thompson

Jawaban:

8

Mengapa keduanya diperlakukan berbeda kebanyakan "karena itulah yang kami butuhkan". Lebih khusus lagi, bentuk argumen tunggal defvarmuncul sejak lama, tetapi kemudian lebih dari yang lain dan pada dasarnya merupakan "retasan" untuk membungkam peringatan kompiler: pada waktu eksekusi itu tidak berpengaruh sama sekali, sehingga sebagai "kecelakaan" itu berarti bahwa perilaku membungkam (defvar FOO)hanya diterapkan pada file saat ini (karena kompiler tidak memiliki cara untuk mengetahui bahwa defvar tersebut telah dieksekusi di beberapa file lain).

Ketika lexical-bindingdiperkenalkan pada Emacs-24, kami memutuskan untuk kembali menggunakan ini (defvar FOO)bentuk, tapi yang menyiratkan bahwa sekarang tidak memiliki efek.

Sebagian untuk mempertahankan perilaku "hanya mempengaruhi file saat ini" sebelumnya, tetapi yang lebih penting untuk memungkinkan perpustakaan untuk digunakan totosebagai var dengan cakupan dinamis tanpa mencegah perpustakaan lain dari penggunaan totosebagai var dengan cakupan leksikal (biasanya konvensi penamaan paket-awalan menghindari yang konflik, tetapi tidak digunakan di mana-mana dengan sedih), perilaku baru (defvar FOO)didefinisikan hanya berlaku untuk file saat ini, dan bahkan disempurnakan sehingga hanya berlaku untuk lingkup saat ini (misalnya jika muncul dalam suatu fungsi, itu hanya mempengaruhi penggunaan var dalam fungsi itu).

Pada dasarnya, (defvar FOO VAL)dan (defvar FOO)hanya dua hal yang "sangat berbeda". Mereka kebetulan menggunakan kata kunci yang sama karena alasan historis.

Stefan
sumber
1
+1 untuk jawabannya. Tetapi pendekatan Common Lisp lebih jelas dan lebih baik, IMHO.
Drew
@Rew: Saya sebagian besar setuju, tetapi menggunakan kembali (defvar FOO)membuat mode baru jauh lebih kompatibel dengan kode lama. Juga, IIRC masalah dengan solusi CommonLisp adalah bahwa itu cukup mahal untuk penerjemah murni seperti Elisp (misalnya setiap kali Anda mengevaluasi letAnda harus melihat ke dalam tubuhnya jika ada declareyang mempengaruhi beberapa vars).
Stefan
Menyetujui kedua hal tersebut.
Drew
4

Berdasarkan eksperimen, saya percaya masalahnya adalah bahwa (defvar VAR)tanpa nilai init hanya berpengaruh pada perpustakaan yang muncul di.

Ketika saya menambahkan (defvar my-dynamic-var)ke *scratch*buffer, kesalahan tidak lagi terjadi.

Saya awalnya berpikir ini karena mengevaluasi formulir itu, tetapi saya kemudian perhatikan pertama-tama bahwa cukup mengunjungi file dengan formulir itu sudah cukup; dan lebih jauh lagi bahwa hanya menambahkan (atau menghapus) yang terbentuk di buffer, tanpa mengevaluasinya cukup untuk mengubah apa yang terjadi ketika mengevaluasi (let ((my-dynamic-var 5)) (f2))di dalam buffer yang sama dengannya eval-last-sexp.

(Saya tidak memiliki pemahaman nyata tentang apa yang terjadi di sini. Saya merasa perilaku ini mengejutkan, tetapi saya tidak mengenal detail bagaimana fungsi ini diimplementasikan.)

Saya akan menambahkan bahwa bentuk defvar(tanpa nilai init) ini mencegah kompiler byte dari mengeluh tentang penggunaan variabel dinamis yang ditentukan secara eksternal dalam file elisp yang sedang dikompilasi, tetapi dengan sendirinya itu tidak menyebabkan variabel itu menjadi boundp; jadi itu tidak sepenuhnya mendefinisikan variabel. (Perhatikan bahwa jika variabel itu boundp maka masalah ini tidak akan terjadi sama sekali.)

Dalam prakteknya Saya kira ini akan bekerja ok asalkan Anda tidak menyertakan (defvar my-dynamic-var)di perpustakaan leksikal mengikat yang menggunakan Anda my-dynamic-varvariabel (yang mungkin akan memiliki definisi yang sebenarnya di tempat lain).


Edit:

Berkat pointer dari @npostavs di komentar:

Keduanya eval-last-sexpdan eval-defundigunakan eval-sexp-add-defvarsuntuk:

Prepend EXP dengan semua defvars yang mendahuluinya di buffer.

Secara khusus ini menempatkan semua defvar, defconstdan defcustomcontoh. (Bahkan ketika berkomentar, saya perhatikan.)

Saat ini sedang mencari buffer pada saat panggilan, itu menjelaskan bagaimana formulir ini dapat memiliki efek dalam buffer bahkan tanpa dievaluasi, dan mengkonfirmasi bahwa formulir tersebut harus muncul dalam file elisp yang sama (dan juga lebih awal dari kode yang dievaluasi) .

phils
sumber
2
IIUC, bug # 18059 mengkonfirmasi percobaan Anda.
Basil
2
Tampaknya eval-sexp-add-defvarsmemeriksa defvars dalam teks buffer.
npostavs
1
+1. Jelas fitur ini tidak jelas, atau tidak disajikan dengan jelas kepada pengguna. Perbaikan doc untuk bug # 18059 membantu, tetapi ini masih merupakan sesuatu yang misterius, jika tidak rapuh, untuk pengguna.
Drew
0

Saya tidak dapat mereproduksi ini sama sekali, mengevaluasi potongan terakhir berfungsi dengan baik di sini dan mengembalikan 5 seperti yang diharapkan. Apakah Anda yakin tidak mengevaluasi my-dynamic-varsendiri? Itu akan melempar kesalahan karena variabelnya batal, itu belum diatur ke nilai dan itu hanya akan memiliki satu jika Anda secara dinamis mengikatnya ke satu.

wasamasa
sumber
1
Apakah Anda menetapkan lexical-bindingnihil sebelum mengevaluasi formulir? Saya mendapatkan perilaku yang Anda gambarkan dengan lexical-bindingnil, tetapi ketika saya mengaturnya ke non-nil, saya mendapatkan kesalahan void-variabel.
Ryan C. Thompson
Ya, saya menyimpan ini ke file terpisah, dikembalikan, diperiksa yang lexical-bindingdiatur dan dievaluasi formulir secara berurutan.
wasamasa
@adalah Reproduces untuk saya, mungkin Anda secara tidak sengaja memberikan my-dynamic-varnilai dinamis tingkat atas di sesi Anda saat ini? Saya pikir itu bisa menandainya spesial secara permanen.
npostavs