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
所以,内部会保证这些符号的唯一性。