gensym 和 uninterned

当 reader 读取到一个 symbol 时,symbol 会被转成大写,然后内部会调用 INTERN 函数去创建新的 symbol。

当 reader 遇到“#:”开头的 symbol 时,不会调用 INTERN 函数,所以这个符号不会添加到当前 package 里。以“#:”打头的符叫 uninterned symbol,所以等于每次在 REPL 输入“#:”的符号时,都等同创建个新的 symbol:

(eq '#:x '#:x)                          ; => NIL

有一个让人疑惑的是 gensym,写宏时时常会用到 gensym 来防止 symbol 冲突:

(defmacro foo ()
  (let ((var (gensym)))
    (let ((,var 1))
       (print ,var))))

执行这段宏:

(foo)                                   ; => 1

预料之中,可以正常输出“1”。再看看 foo 这个宏展开后的样子:

(macroexpand-1 '(foo))
;; =>
;; (LET ((#:G1075 1))
;;   (PRINT #:G1075))
;; T

可以看到 gensym 生成了以“#:”打头 symbol,#:G1075 是 uninterned symbols,但为什么在宏里能执行呢?如果把这段宏展开后的代码直接粘贴到 REPL 里执行一下,REPL 会提示:

(LET ((#:G1075 1))
  (PRINT #:G1075))             ; => T
                               ; The variable #:G1075 is unbound

这是意料之中的,**因为你拷贝过去的代码中的 symbol 直接是由 reader 读取的**,reader 读到 #:G1075时,它成为一个 uninterned symbols——这个 symbol 不会注册到 package 里的,所以 PRINT 的时候是不能打印的。

而宏展开后的并不是表面看到的那样,在分析前,我们首先要改一个变量:

(setf *print-circle* t)

然后再看看展开后的模样:

(macroexpand-1 '(foo))
;; =>
;; (LET ((#1=#:G1076 1))
;;   (PRINT #1#))
;; T

看到了吧,这次展开后看到的 symbol 跟之前是不一样的格式。gensym 保证了生成的 symbol 名是唯一的,因为内部会处理好的,你就当它是内部变量吧,所以你直接用“#:”开头 symbol 是不行的:

(defvar #:x 1)
#:x                                  ; => The variable #:X is unbound.

再看看 (defvar #:x 1) 的宏展开:

(macroexpand-1 '(defvar #:x 1))
;; =>
;; (PROGN
;;   (EVAL-WHEN (:COMPILE-TOPLEVEL) (SB-IMPL::%COMPILER-DEFVAR '#1=#:X))
;;   (EVAL-WHEN (:LOAD-TOPLEVEL :EXECUTE)
;;     (SB-IMPL::%DEFVAR '#1# (UNLESS (BOUNDP '#1#) 1) 'T NIL 'NIL
;;                       (SB-C:SOURCE-LOCATION))))
;; T

这说明 Lisp 内部也不是使用的“#:”打头的 symbol 名,而是用 #1# 直接引用的这 symbol。比如:

(eq '#1=#:G2052 '#1#)                   ; => T

所以,内部会保证这些符号的唯一性。