为什么 defpackage 使用 uninterned symbols
有一些库的作者定义 package 名字时,使用了 uninterned symbols,如:
(defpackage #:one-package (:use cl))
然后再用这个符号进入这个包:
(in-package #:one-package)
按语言标准描述,#: 是 uninterned symbols,意味着每次被 reader 读取并创建的符号是不会被添加到当前 package 中,而是创建一个新的 symbol,等于是一个一次性的 symbol。这里迷惑了很多人——为什么要用 uninterned symbols?
实际 defpackage 会把符号转换成大写字符串,上面代码其实创建了一个叫 ONE-PACKAGE 的包,将 defpackage(使用的 SBCL)宏展开后如下:
(EVAL-WHEN (:COMPILE-TOPLEVEL :LOAD-TOPLEVEL :EXECUTE) (SB-IMPL::%DEFPACKAGE "ONE-PACKAGE" 'NIL 'NIL 'NIL 'NIL '("CL") 'NIL 'NIL 'NIL '("ONE-PACKAGE") 'NIL 'NIL 'NIL (SB-C:SOURCE-LOCATION)))
估计这里你又要迷糊了,uninterned symbols 怎么就变成字符串了?
别看书了,看一下 SBCL 源码中 defpackage 的实现:
;; In src/code/defpackage.lisp (defmacro defpackage (package &rest options) ... (let ( ... (implement (stringify-package-designators (list package))) ...)) ...)
如上,package 名字会被放入一个列表中,然后被 stringify-package-designators 函数调用,stringify-package-designators 的实现如下:
(defun stringify-package-designators (package-designators) (mapcar #'stringify-package-designator package-designators))
这样每个元素会被 stringify-package-designator 调用,再看 stringify-package-designator 的实现:
(defun stringify-package-designator (package-designator) (typecase package-designator (simple-string package-designator) (string (coerce package-designator 'simple-string)) (symbol (symbol-name package-designator)) (character (string package-designator)) (package (package-name package-designator)) (t (error "~S does not designate a package" package-designator))))
是的,package 名字允许几种类型:simple-string、string、symbol、character 和 package,然后它们均被相应的函数给转换成字符串了。
这就是 defpackage 的 package 名实际被转换成字符串的原因。
那为什么要用 uninterned symbols 去当作包名字,理由很简单,因为我们输入的符号会被 INTERN 到当前包,这样就创建一个新符号对象。使用 #: 是不会创建新符号,而包名要的是一个字符串,又不要你创建新符号,对一些有代码洁癖的 Lisp 黑客,这很抓狂。
SBCL 里,package 名字会被存储到一个 hash 表中,这个 hash 表在 sb-impl::*package-names* 里(分析源码就能找到了),通过遍历它的 key,就可以看到当前所有 package 名字了:
(loop for k being the hash-key in sb-impl::*package-names* do (print k)) ;; => ;; "SB-EVAL" ;; "SB-WALKER" ;; "SB-VM" ;; "SB-UNIX" ;; "SB-SYS" ;; ......
从上面输出结果可以看到,包名确实是字符串形式存储的。
另外说一句:由于 SBCL(以及很多其他 Common Lisp 实现)的有一大部分都是用 Common Lisp 实现的,所以有时调试语言内核会变得非常方便。