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