gensym 和 uninterned
当 reader 读取到一个 symbol 时,symbol 会被转成大写,然后内部会调用 INTERN 函数去创建新的 symbol。
当 reader 遇到“#:”开头的 symbol 时,不会调用 INTERN 函数,所以这个符号不会添加到当前 package 里。以“#:”打头的符叫 uninterned symbol,所以等于每次在 REPL 输入“#:”的符号时,都等同创建个新的 symbol:
CL-USER> (eq '#:x '#:x) NIL
有一个让人疑惑的是 gensym,写宏时时常会用到 gensym 来防止 symbol 冲突:
(defmacro foo () (let ((var (gensym))) (let ((,var 1)) (print ,var))))
执行这段宏:
CL-USER> (foo) 1
预料之中,可以正常输出“1”。再看看 foo 这个宏展开后的样子:
CL-USER> (macroexpand-1 '(foo)) (LET ((#:G1075 1)) (PRINT #:G1075)) T
可以看到 gensym 生成了以“#:”打头 symbol,#:G1075 是 uninterned symbols,但为什么在宏里能执行呢?如果把这段宏展开后的代码直接粘贴到 REPL 里执行一下,REPL 会提示:
CL-USER> (LET ((#:G1075 1)) (PRINT #:G1075)) T The variable #:G1075 is unbound
这是意料之中的,**因为你拷贝过去的代码中的 symbol 直接是由 reader 读取的**,reader 读到 #:G1075时,它成为一个 uninterned symbols——这个 symbol 不会注册到 package 里的,所以 PRINT 的时候是不能打印的。
而宏展开后的并不是表面看到的那样,在分析前,我们首先要改一个变量:
(setf *print-circle* t)
然后再看看展开后的模样:
CL-USER> (macroexpand-1 '(foo)) (LET ((#1=#:G1076 1)) (PRINT #1#)) T
看到了吧,这次展开后看到的 symbol 跟之前是不一样的格式。gensym 保证了生成的 symbol 名是唯一的,因为内部会处理好的,你就当它是内部变量吧,所以你直接用“#:”开头 symbol 是不行的:
CL-USER> (defvar #:x 1) #:X CL-USER> #:x The variable #:X is unbound.
再看看 (defvar #:x 1) 的宏展开:
CL-USER> (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。比如:
CL-USER> (eq '#1=#:G2052 '#1#) T
所以,内部会保证这些符号的唯一性。