为什么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名字了:

CL-USER> (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实现的,所以有时调试语言内核会变得非常方便。