This answer is an extension of Phils answer.
Your question shows the potential danger of locally binding special-declared variables.
The convention of prefixing symbol names with library names normally protects us from accidentally using special-declared symbols for local bindings that should have lexical scope.
But there are exceptions like displayed-month
and displayed-year
from calendar.el
.
One can ensure lexical binding with the help of uninterned symbols.
The principle is described through a simple documented example:
;; -*- lexical-binding: t -*-
(require 'cl)
;; reset:
(unintern 'wtf)
(unintern 'foo)
;; declare `wtf' as special:
(defvar wtf)
(setq wtf 0)
(defmacro f ()
(let ((sym (make-symbol "wtf"))) ;; make-symbol' returns an uninterned symbol with name
wtf'
(let ((,sym 1)) ;; We substitute here the uninterned symbol
wtf'.
(lambda ()
,sym))))
(setq ans (funcall (f)))
;; => 1
It is clear that the definition of the macro f
in the above example is cumbersome.
But a special lexlet
macro as defined in the following can relieve us from that work.
First comes the library code defining the lexlet
macro and below that the user code resembling the simplified introductory example.
The macro lexlet
generates uninterned duplicates of all locally bound symbols, locally binds the uninterned symbols to their given values and replaces the locally bound interned symbols in the body with their uninterned counterparts.
The definition of the macro is kept simple. But there is a small caveat. One cannot use the function definition of the locally bound symbols.
;; -*- lexical-binding: t; -*-
;; Here starts the library code.
(defun lexlet--search (data symbol-alist)
"Replace symbols according to SYMBOL-ALIST in DATA."
(cond
((symbolp data)
(or
(alist-get data symbol-alist)
data))
((vectorp data)
(apply #'vector (seq-map #'lexlet--search data symbol-alist)))
((consp data)
(cons (lexlet--search (car data) symbol-alist)
(lexlet--search (cdr data) symbol-alist)))
(t data)))
;; Test:
;; (lexlet--search '(lambda () wtf) '((wtf . var)))
(defmacro lexlet (bindings &rest body)
"Like `let' but with lexical binding for all symbols in BINDINGS.
If a symbol has a local binding in BINDINGS its binding during
the execution of BODY is lexical even if the symbol is declared as special.
This macro requires `lexical-binding' set to t."
(declare (indent 1)
(debug ((&rest (symbol sexp)) body)))
(let* ((symbol-alist (mapcar
(lambda (binding)
(let ((symbol (car binding)))
(cons
symbol
(make-symbol (symbol-name symbol)))))
bindings))
(new-bindings (mapcar
(lambda (binding)
(let* ((old-symbol (car binding))
(new-symbol (alist-get old-symbol symbol-alist)))
(cons new-symbol (cdr binding))))
bindings)))
(append
(list
#'let
new-bindings)
(lexlet--search body symbol-alist))))
(provide 'lexlet)
With lexlet
the user code of the introductory example looks friendlier:
(require 'lexlet)
(defvar wtf)
(setq wtf 0)
(defun f ()
(lexlet ((wtf 1))
(lambda ()
wtf)))
(setq ans (funcall (f)))
;; => 1
It is important that the macro extends the lexical environment created by an outer let
.
For an example eval
with an environment given as argument lexical
does not extend an outer lexical environment and is therefore only of limited use for our purpose.
But, lexlet
is okay in that regard:
;; -*- lexical-binding: t -*-
(require 'lexlet)
;; reset:
(unintern 'wtf)
(unintern 'lexbound)
(defvar wtf)
(setq wft 0)
(defun f ()
(let ((lexbound 1))
(lexlet ((wtf 2))
(lambda ()
(list
lexbound wtf)))))
(setq ans (funcall (f)))
;; => (1 2)
fset
. The enclosing lambda can be defined andfset
by adefun
as in your case. But you are right, the behavior is wired. – Tobias Mar 16 '23 at 08:46lambda
is self-quoting. Furthermore, for repeatability it is better to separate the marking of the symbol as special and the binding of the symbol to a value:(defvar wtf) (setq wtf 10)
. – Tobias Mar 16 '23 at 08:48#'
or not. I used to write some CL code and I used#'
there. It seems that Elisp doesn't refuse this notation, so I continue to do this way because it makes lambda expressions more prominent. Thanks for your suggestion; without#'
, the code may look cleaner. – shynur Mar 16 '23 at 09:32