Por que o escopo defvar funciona de maneira diferente sem um valor init?

10

Suponha que eu tenha um arquivo chamado elisp-defvar-test.elcontendo:

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

Carrego esse arquivo e, em seguida, vou para o buffer temporário e execute:

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

(f1 5)retorna 5 conforme o esperado, indicando que o corpo de f1está tratando my-dynamic-varcomo uma variável com escopo dinâmico, conforme o esperado. No entanto, a última forma fornece um erro de variável nula para my-dynamic-var, indicando que está usando o escopo lexical para essa variável. Isso parece estar em desacordo com a documentação de defvar, que diz:

O defvarformulário também declara a variável como "especial", para que ela seja sempre vinculada dinamicamente, mesmo que lexical-bindingseja t.

Se eu alterar o defvarformulário no arquivo de teste para fornecer um valor inicial, a variável será sempre tratada como dinâmica, como diz a documentação. Alguém pode explicar por que o escopo de uma variável é determinado pelo fato de ter ou não defvarsido fornecido um valor inicial ao declarar essa variável?

Aqui está o erro de rastreamento, caso isso importe:

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
fonte
4
Eu acho que a discussão no bug # 18059 é relevante.
24918 Basil Basil
Ótima pergunta, e sim, consulte a discussão do bug # 18059.
Tirou
Vejo, então parece que a documentação será atualizado para resolver este em Emacs 26.
Ryan C. Thompson

Respostas:

8

O motivo pelo qual os dois são tratados de maneira diferente é principalmente "porque é disso que precisamos". Mais especificamente, a forma de argumento único defvarapareceu há muito tempo, mas mais tarde que a outra e era basicamente um "truque" para silenciar os avisos do compilador: no momento da execução, ela não teve nenhum efeito, portanto, como "acidente", significou que o comportamento de silenciamento se (defvar FOO)aplicava apenas ao arquivo atual (uma vez que o compilador não tinha como saber que esse defvar havia sido executado em outro arquivo).

Quando lexical-bindingfoi introduzido no Emacs-24, decidimos reutilizar este (defvar FOO)formulário, mas isso implica que agora ele tem um efeito.

Em parte para preservar o comportamento anterior "afeta apenas o arquivo atual", mas o mais importante é permitir que uma biblioteca use totocomo um var de escopo dinâmico sem impedir que outras bibliotecas usem totocomo um var de escopo lexicamente (geralmente a convenção de nomenclatura de prefixo de pacote evita esses conflitos, mas infelizmente não é usado em todos os lugares), o novo comportamento de (defvar FOO)foi definido para aplicar-se apenas ao arquivo atual e foi refinado, portanto, somente se aplica ao escopo atual (por exemplo, se ele aparecer em uma função, afeta apenas os usos de que var dentro dessa função).

Fundamentalmente, (defvar FOO VAL)e (defvar FOO)são apenas duas coisas "completamente diferentes". Por acaso, eles usam a mesma palavra-chave por razões históricas.

Stefan
fonte
11
+1 para a resposta. Mas a abordagem do Common Lisp é mais clara e melhor, IMHO.
Tirou
@ Drew: Eu concordo principalmente, mas a reutilização do (defvar FOO)torna o novo modo muito mais compatível com o código antigo. Além disso, o IIRC que tem um problema com a solução do CommonLisp é que é muito caro para um intérprete puro como o do Elisp (por exemplo, toda vez que você avalia um, leté necessário olhar dentro de seu corpo, caso haja algum declareque afete alguns dos outros).
23418 Stefan
Concordou em ambas as acusações.
21418 Drew
4

Com base na experimentação, acredito que o problema é que, (defvar VAR)sem nenhum valor init, apenas afeta as bibliotecas em que aparece.

Quando adicionei (defvar my-dynamic-var)ao *scratch*buffer, o erro não ocorreu mais.

Originalmente, pensei que isso se devia à avaliação desse formulário, mas notei, em primeiro lugar, que basta visitar o arquivo com esse formulário presente; e, além disso, o simples fato de adicionar (ou remover) esse formulário no buffer, sem avaliar, foi suficiente para alterar o que aconteceu ao avaliar (let ((my-dynamic-var 5)) (f2))dentro desse mesmo buffer com eval-last-sexp.

(Não tenho uma compreensão real do que está acontecendo aqui. Acho o comportamento surpreendente, mas não estou familiarizado com os detalhes de como essa funcionalidade é implementada.)

Acrescentarei que essa forma de defvar(sem valor init) impede o compilador de bytes de reclamar sobre o uso de uma variável dinâmica definida externamente no arquivo elisp que está sendo compilado, mas por si só não faz com que essa variável seja boundp; portanto, não está definindo estritamente a variável. (Observe que, se a variável fosse boundp , esse problema não ocorreria.)

Na prática Suponho que isto vai funcionar ok, desde que você não incluem (defvar my-dynamic-var)em qualquer biblioteca de ligação lexical que usa sua my-dynamic-varvariável (que presumivelmente teria uma definição real em outro lugar).


Editar:

Graças ao ponteiro de @npostavs nos comentários:

Ambos eval-last-sexpe eval-defunuse eval-sexp-add-defvarspara:

Anexe EXP a todos os defvars que o precedem no buffer.

Especificamente ele localiza todos defvar, defconste defcustomcasos. (Mesmo quando comentado, eu noto.)

Como ele está pesquisando o buffer no momento da chamada, explica como esses formulários podem ter efeito no buffer, mesmo sem serem avaliados, e confirma que o formulário deve aparecer no mesmo arquivo elisp (e também antes do código que está sendo avaliado) .

phils
fonte
2
IIUC, bug # 18059 confirma seus esforços.
24918 Basil Basil
2
Parece que eval-sexp-add-defvarsverifica se há defvars no texto do buffer.
npostavs
11
+1. Claramente, esse recurso não é claro ou não é apresentado claramente aos usuários. A correção de documento do bug # 18059 ajuda, mas isso ainda é algo misterioso, se não frágil, para os usuários.
Tirou
0

Não consigo reproduzir isso, avaliar o último trecho funciona bem aqui e retorna 5 conforme o esperado. Tem certeza de que não está avaliando my-dynamic-varpor conta própria? Isso gerará um erro porque a variável é nula, não foi definida como um valor e só terá um se você a vincular dinamicamente a um.

wasamasa
fonte
11
Você definiu lexical-bindingnulo antes de avaliar os formulários? Recebo o comportamento que você descreve com lexical-bindingzero, mas quando o configuro como zero, recebo o erro de variável nula.
Ryan C. Thompson
Sim, salvei isso em um arquivo separado, revertido, verificado se lexical-bindingestá definido e avaliado os formulários sequencialmente.
Wasamasa
@wasamasa Reproduz para mim, talvez você tenha acidentalmente atribuído my-dynamic-varum valor dinâmico de nível superior em sua sessão atual? Eu acho que isso poderia marcá-lo permanentemente especial.
npostavs