¿Por qué el alcance de defvar funciona de manera diferente sin un valor init?

10

Supongamos que tengo un archivo llamado que elisp-defvar-test.elcontiene:

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

Cargo este archivo y luego entro en el búfer de memoria virtual y ejecuto:

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

(f1 5)devuelve 5 como se esperaba, lo que indica que el cuerpo de f1se trata my-dynamic-varcomo una variable de ámbito dinámico, como se esperaba. Sin embargo, la última forma da un error de variable nula para my-dynamic-var, lo que indica que está utilizando el alcance léxico para esta variable. Esto parece estar en desacuerdo con la documentación para defvar, que dice:

El defvarformulario también declara la variable como "especial", de modo que siempre está unida dinámicamente incluso si lexical-bindinges t.

Si cambio el defvarformulario en el archivo de prueba para proporcionar un valor inicial, entonces la variable siempre se trata como dinámica, como dice la documentación. ¿Alguien puede explicar por qué el alcance de una variable está determinado por si se defvarle proporcionó o no un valor inicial al declarar esa variable?

Aquí está el error de seguimiento, en caso de que sea 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)
Ryan C. Thompson
fuente
44
Creo que la discusión en el error # 18059 es relevante.
Albahaca
Gran pregunta, y sí, sírvase ver la discusión del error # 18059.
Dibujó
Ya veo, por lo que parece que la documentación se actualizará para abordar esto en Emacs 26.
Ryan C. Thompson

Respuestas:

8

Por qué los dos son tratados de manera diferente es principalmente "porque eso es lo que necesitábamos". Más específicamente, la forma de argumento único defvarapareció hace mucho tiempo, pero más tarde que la otra y fue básicamente un "truco" para silenciar las advertencias del compilador: en el momento de la ejecución no tuvo ningún efecto, por lo que como un "accidente" significó que el comportamiento de silenciamiento (defvar FOO)solo se aplica al archivo actual (ya que el compilador no tenía forma de saber que tal defvar se había ejecutado en otro archivo).

Cuando lexical-bindingse introdujo en Emacs-24, decidimos volver a utilizar esta (defvar FOO)forma, sino que implica que ahora lo hace tener un efecto.

En parte para preservar el comportamiento anterior de "solo afecta el archivo actual", pero lo más importante es permitir que una biblioteca lo use totocomo una variable de ámbito dinámico sin evitar que otras bibliotecas lo utilicen totocomo una variable de ámbito léxico (por lo general, la convención de nomenclatura de prefijo de paquete evita esos conflictos, pero lamentablemente no se usa en todas partes), el nuevo comportamiento de (defvar FOO)se definió para aplicarse solo al archivo actual, e incluso se refinó para que solo se aplique al alcance actual (por ejemplo, si aparece dentro de una función, solo afecta los usos de esa var dentro de esa función).

Fundamentalmente, (defvar FOO VAL)y (defvar FOO)son solo dos cosas "completamente diferentes". Simplemente usan la misma palabra clave por razones históricas.

Stefan
fuente
1
+1 por la respuesta. Pero el enfoque de Common Lisp es más claro y mejor, en mi humilde opinión.
Dibujó
@Drew: Estoy de acuerdo en su mayoría, pero la reutilización (defvar FOO)hace que el nuevo modo sea mucho más compatible con el código anterior. Además, IIRC, un problema con la solución de CommonLisp es que es bastante costoso para un intérprete puro como el de Elisp (por ejemplo, cada vez que evalúa un lettiene que mirar dentro de su cuerpo en caso de que haya algo declareque afecte a algunos de los vars).
Stefan
De acuerdo en ambos aspectos.
Dibujó el
4

Basado en la experimentación, creo que el problema es que (defvar VAR)sin ningún valor de inicio solo tiene un efecto en las bibliotecas en las que aparece.

Cuando agregué (defvar my-dynamic-var)al *scratch*búfer, el error ya no ocurrió.

Originalmente pensé que esto se debía a la evaluación de ese formulario, pero luego noté en primer lugar que simplemente visitar el archivo con ese formulario presente era suficiente; y además que simplemente agregar (o eliminar) esa forma en el búfer, sin evaluarlo, fue suficiente para cambiar lo que sucedió al evaluar (let ((my-dynamic-var 5)) (f2))dentro de ese mismo búfer con eval-last-sexp.

(No tengo una comprensión real de lo que está sucediendo aquí. El comportamiento me parece sorprendente, pero no estoy familiarizado con los detalles de cómo se implementa esta funcionalidad).

Agregaré que esta forma de defvar(sin valor de inicio) evita que el compilador de bytes se queje de los usos de una variable dinámica definida externamente en el archivo elisp que se está compilando, pero por sí solo no causa que esa variable sea boundp; entonces no está definiendo estrictamente la variable. (Tenga en cuenta que si la variable fuera boundp entonces este problema no ocurriría en absoluto).

En la práctica, supongo que esto va a funcionar bien, siempre que no incluya (defvar my-dynamic-var)en cualquier biblioteca de unión al léxico que utiliza su my-dynamic-varvariable (que presumiblemente tendría una definición real en otro lugar).


Editar:

Gracias al puntero de @npostavs en los comentarios:

Ambos eval-last-sexpy eval-defunusar eval-sexp-add-defvarspara:

Anteponga EXP con todos los defvars que lo preceden en el búfer.

Específicamente, se localiza todos defvar, defconsty defcustomcasos. (Incluso cuando comentado, me doy cuenta).

Como esto busca en el búfer en el momento de la llamada, explica cómo estos formularios pueden tener un efecto en el búfer incluso sin ser evaluados, y confirma que el formulario debe aparecer en el mismo archivo elisp (y también antes del código que se está evaluando) .

phils
fuente
2
IIUC, error # 18059 confirma sus ejercicios.
Albahaca
2
Parece que eval-sexp-add-defvarsbusca defvars en el texto del búfer.
npostavs
1
+1. Claramente, esta característica no está clara o no se presenta claramente a los usuarios. La corrección de documentos para el error # 18059 ayuda, pero esto sigue siendo algo misterioso, si no frágil, para los usuarios.
Dibujó
0

No puedo reproducir esto en absoluto, evaluar el último fragmento funciona bien aquí y devuelve 5 como se esperaba. ¿Estás seguro de que no estás evaluando my-dynamic-varsolo? Eso arrojará un error porque la variable es nula, no se ha establecido en un valor y solo tendrá uno si lo vincula dinámicamente a uno.

wasamasa
fuente
1
¿Estableció lexical-bindingno nulo antes de evaluar los formularios? Obtengo el comportamiento que usted describe con lexical-bindingnil, pero cuando lo configuro en nil, obtengo el error de variable vacío.
Ryan C. Thompson
Sí, guardé esto en un archivo separado, lo revertí, verifiqué lexical-bindingy configuré los formularios secuencialmente.
wasamasa
@wasamasa Reproduce para mí, ¿tal vez accidentalmente has dado my-dynamic-varun valor dinámico de alto nivel en tu sesión actual? Creo que eso podría marcarlo permanentemente especial.
npostavs