Почему определение области видимости по-разному работает без инициализации?

10

Предположим, у меня есть файл с именем 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

Я загружаю этот файл, затем захожу в рабочий буфер и запускаю:

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

(f1 5)возвращает 5, как и ожидалось, указывая, что тело f1обрабатывается my-dynamic-varкак переменная с динамической областью, как и ожидалось. Тем не менее, последняя форма выдает ошибку void-variable my-dynamic-var, указывая на то, что она использует лексическую область видимости для этой переменной. Это, кажется, расходится с документацией defvar, которая гласит:

defvarФорма также объявляет переменную как «специальный», так что она всегда динамически связана , даже если lexical-bindingэто т.

Если я изменю defvarформу в тестовом файле, чтобы указать начальное значение, то переменная всегда обрабатывается как динамическая, как сказано в документации. Кто-нибудь может объяснить, почему область видимости переменной определяется тем, было ли defvarзадано начальное значение при объявлении этой переменной?

Вот обратная трассировка ошибки, если она имеет значение:

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
Я думаю, что обсуждение в баге № 18059 является актуальным.
Василий
Отличный вопрос, и да, пожалуйста, смотрите обсуждение ошибки # 18059.
Дрю
Понятно, что, похоже, документация будет обновлена ​​для решения этой проблемы в Emacs 26.
Райан С. Томпсон,

Ответы:

8

Почему к обоим относятся по-разному, в основном «потому что это то, что нам было нужно». Точнее говоря, форма с одним аргументом defvarпоявилась очень давно, но позже, чем другие, и была в основном «хаком», чтобы заставить замолчать предупреждения компилятора: во время выполнения она вообще не имела никакого эффекта, поэтому как «случайность» она означала что поведение молчания (defvar FOO)применяется только к текущему файлу (поскольку компилятор не может знать, что такой defvar был выполнен в каком-то другом файле).

Когда lexical-bindingбыл введен в Emacs-24, мы решили повторно использовать эту (defvar FOO)форму, но это означает , что он теперь делает иметь эффект.

Частично, чтобы сохранить предыдущее поведение «влияет только на текущий файл», но, что более важно, разрешить библиотеке использовать totoв качестве динамически изменяемой переменной, не предотвращая использование другими библиотеками totoв качестве лексически изменяемой переменной (обычно соглашение об именовании префиксов пакетов позволяет избежать конфликтует, но это не везде используется, к сожалению), новое поведение (defvar FOO)было определено, чтобы применяться только к текущему файлу, и даже было уточнено, поэтому оно применяется только к текущей области (например, если оно появляется внутри функции, оно влияет только на использование что вар в этой функции).

Принципиально (defvar FOO VAL)и (defvar FOO)это просто две «совершенно разные» вещи. Они просто используют одно и то же ключевое слово по историческим причинам.

Стефан
источник
1
+1 за ответ. Но подход Common Lisp яснее и лучше, ИМХО.
Дрю
@Drew: Я в основном согласен, но повторное использование (defvar FOO)делает новый режим намного более совместимым со старым кодом. Кроме того, проблема IIRC с решением CommonLisp заключается в том, что для чистого интерпретатора, подобного Elisp, это довольно дорого (например, каждый раз, когда вы оцениваете, letвы должны заглянуть внутрь его тела на случай, если declareчто-то повлияет на некоторые переменные).
Стефан
Договорились по обоим пунктам.
Дрю
4

Основываясь на экспериментах, я считаю, что проблема заключается в том, что (defvar VAR)отсутствие значения init влияет только на библиотеки, в которых оно появляется.

Когда я добавил (defvar my-dynamic-var)в *scratch*буфер, ошибка больше не возникала.

Первоначально я думал, что это из-за оценки этой формы, но затем я заметил, во-первых, что достаточно просто посетить файл с этой формой; и, кроме того, простого добавления (или удаления) этой формы в буфере без оценки этого было достаточно, чтобы изменить то, что произошло при оценке (let ((my-dynamic-var 5)) (f2))внутри этого же буфера с помощью eval-last-sexp.

(У меня нет реального понимания того, что здесь происходит. Я нахожу поведение удивительным, но я не знаком с деталями того, как эта функциональность реализована.)

Я добавлю, что эта форма defvar(без значения init) не позволяет байт-компилятору жаловаться на использование внешней определенной динамической переменной в компилируемом файле elisp, но сама по себе она не приводит к тому, что эта переменная будет boundp; так что это не строго определение переменной. (Обратите внимание, что если бы переменная была boundp тогда, эта проблема не возникла бы вообще.)

На практике я предполагаю , что это будет работать КИ при условии , что вы действительно включаете (defvar my-dynamic-var)в любой лексическом связывании библиотеки , которая использует ваш my-dynamic-varпеременную (которые , предположительно , будут иметь реальное определение в другом месте).


Редактировать:

Благодаря указателю из @npostavs в комментариях:

И то, eval-last-sexpи другое eval-defunиспользуется eval-sexp-add-defvarsдля того, чтобы:

Prepend EXP со всеми defvars, которые предшествуют ему в буфере.

В частности , она находит все defvar, defconstи defcustomэкземпляры. (Даже когда закомментировано, я замечаю.)

Поскольку это поиск в буфере во время вызова, он объясняет, как эти формы могут оказывать влияние в буфере, даже не будучи оцененным, и подтверждает, что форма должна появиться в том же файле elisp (а также раньше, чем оцениваемый код) ,

Phils
источник
2
IIUC, ошибка # 18059 подтверждает ваши эксперименты.
Василий
2
Кажется, что eval-sexp-add-defvarsпроверяет наличие defvars в тексте буфера.
npostavs
1
+1. Понятно, что эта функция не ясна или не представлена ​​пользователям. Помогает документальное исправление для ошибки # 18059, но это все еще что-то загадочное, если не хрупкое, для пользователей.
Дрю
0

Я не могу воспроизвести это вообще, оценка последнего фрагмента работает отлично и возвращает 5, как и ожидалось. Вы уверены, что не оцениваете my-dynamic-varсами по себе? Это приведет к ошибке, поскольку переменная void, ей не присвоено значение, и она будет иметь только одно значение, если вы динамически связываете его с одним.

wasamasa
источник
1
Вы установили lexical-bindingне ноль перед оценкой форм? Я получаю поведение, которое вы описываете с помощью lexical-bindingnil, но когда я устанавливаю его не равным nil, я получаю ошибку void-variable.
Райан К. Томпсон
Да, я сохранил это в отдельный файл, отменил, проверил, который lexical-bindingустановлен и оценил формы последовательно.
Васамаса
@wasamasa Воспроизводит для меня, может быть, вы случайно указали my-dynamic-varдинамическое значение верхнего уровня в текущем сеансе? Я думаю, что это может отметить это постоянно особенным.
npostavs