1 读取器如何处理包
每个包都有名字,(find-package name)可以通过包名来找到一个包。解释器当前使用包存储于全局变量*package*中。包的字面形式为#<Package "pkg-name">。
(package-name pkg)可以返回一个包的名称。
包可看作一个字符串和符号的映射表,读取器使用(find-symbol name)和intern通过名字来查找特定包中的某个符号。二者的区分是后者会在符号不存在时,在包中创建这个新的符号。反向的(symbol-name symb)将反查一个符号的名字字符串表示。
通常我们使用的名字大多是非限定的,即名字里不带冒号。读取器遇到这种名字时,会将名字中所有未转义的字母转换为大写形式,然后将其传递给Intern,为名字返回一个符号对象。
含有单冒号的名字指向外部符号,即这些符号被包导出为外部可见的符号。当读取器遇到这类名字中,将名字在:号外拆分,前一部分作为包名,后一部分作为符号名,读取器查找适当的包并用它来将名称转换成符号对象。如果命名的包中不存在对应名称的符号或符号并不是外部可见,则读取器会出错。
关键字符号是另外一种含有单冒号的名字,这类符号在包为KEYWORD的包中创建并自动导出。
含有双冒号的名字可指向包中的任何符号,这通常不是好作法-因为这样可以访问到本意保留为私有的符号。
可以在REPL中,输入cl:和cl::后按tab,能看到包中的符号名称。
以#:开始的名字为unintern符号,即符号不属于任何包,每当读取器遇到这样的名字时,都会重新为此名字创建一个符号。gensym返回的就是这种名字。
2 符号与包相关术语
可访问符号:在一个给定包中,可通过find-symbol找到的符号
有两种方式使得一个符号在包中可访问。
一种是符号归属于一个包,称这个包是符号的home-package,这样包的"名称-符号"表中存在此符号的项。(package-name (symbol-package 'sym-name))可以查找一个符号最初来源于哪个包。
另一种方式是当一个包通过use使用另一个包时,前者便继承了后者导出的外部符号,从而可访问继承的符号,使得这些符号进入前者的"名称-符号"表中,称为导入。这样一个符号可以同时存在于多个包中。
包系统只允许每个名字在给定的包中指向单个符号。这样如果继承自多个包中有重名符号,或者自身定义有和使用的包中重名的符号是不允许的。必须将冲突的名称之一进行屏蔽(shadow)。每个包都维护了一个屏蔽符号表。
一个已有的符号可以从包中unintern,这会导致其被清除出"名称-符号"表,如果它是一个隐蔽符号,其也会被清除出隐蔽符号列表。
3 标准包
COMMON-LISP-USER包,也叫CL-USER包,是Lisp环境的默认载入包。所有用户自定义的符号会被导入其中。
COMMON-LISP包,这个包导出了语言标准定义的所有名字,其被CL-USER包所使用。
KEYWORD包,这个包被Lisp读取器用来创建以冒号开始的名字。
4 使用包
与Java的包机制不同,CL的包无法提供诸如谁可以调用什么函数或访问什么变量的直接控制。其只是通过控制如何将文本名转换为称号对象来提供对于名字空间的基本控制。
自定义包通过defpackge来完成:
(defpackage :cn.edu.bupt.some-name
(:use :common-lisp ...:other-package)
(:import-from :the-package :the-oper1 ..)
(:shadow :op :op ...)
(:export :oper1 :oper2 ... :opern))
包的命名类似于Java世界的作法,采用URI的反转形式。
use指定需要继承的包,系统默认继承COMMON-LISP包。包的写法可以是关键字法,这时允许使用小写字符。如果为字符串,则需要使用大写形式,如”COMMON-LISP"。
如果只需要导入特定包中的某些符号,则使用import-from的形式。可以有多个子句以从不同的包中导入。有时只需要屏蔽包中的少数符号,此时可以直接使用整个包,但把不需要的符号通过:shadow进行屏蔽。
有时从多个包继承时会遇到名字冲突,但我们需要其中的一个符号,此时使用:shoadow-import-from来导入一个冲突的符号。
需要导出符号的可以使用:export来进行说明。这样在其使用者包中,可以直接访问,而无需采用限定名的方式访问。
(in-package :name) 可以切换到指定包的上下文中,直到另一个in-package调用来改变它。其是一个宏。
此外函数或宏,如(use-package),其反转操作为(unuse-package)。(export ),其反转操作为(unexport )。(import )等,均可defpackage完成,不建议使用这些东西。
5 包定义的组织
系统要求包在它们被用到之前总是存在。最佳方式是把包定义与使用它们的源代码放到分开的文件里。
一种方式是每个包定义一个文件,另一种是把一系列包定义放在一个文件中。前者对文件的加载顺序提出了要求。
编译时,必须先加载包定义,再编译从包里读取符号的文件。对于大型项目,需要使用项目组织工具如ASDF来处理加载顺序这一问题。
另一个问题是每个文件只应该含有一个in-package形式,并且其为除注释以外的第一行。含有包定义的文件,应当以(in-package :common-lisp-user)的形式开始。
最后要注意的是包名也有冲突的可能性。
6 一些难题
第一个难题由于不经意的intern导致名字冲突的问题。例如未导入包时访问一个符号,由于此符号未定义,因而读取器会创建之,并调用求值器,求值器会导致系统会进入再启动,恢复之后导入包时,再调用同名符号时会遇到名称冲突的问题。此时需要清除先前引入的符号定义。
第二个难题是使用一个包后,新定义的符号和使用包的名称冲突,此时编译时会提示重定义,需要对其进行解决。
第三个难题是切换到其他包里工作,但未切换回CL-USER包,此时quit指令失效。
最后牢记的是:包是读取器的一部分,不是求值器的部分。
7 更多关于符号
符号是Lisp中的一种原子数据类型,每个符号都是一个对象。和数一样,符号的含义依赖于如何解释它。通常符号不对自身求值,因此对于符号需要用'引用它。读取器会将其转换为大写形式。符号可以使用字符串来当作其名字,通过symbol-name可以获取符号的名字。
每个符号都有一个属性列表,称为plist。其实际上是一个键值对,可以用get来访问其中的键值。
(get symbol 'key)
可以使用setf来设置键值。属性列表。例如可以为color键设置为red。
(setf (get symbol 'color) red)
结合包的名称-符号表,符号有如下的结构:
一个令人困惑的事情是,符号与变量从两个不同的层面上存在关联。
当符号是special variable的名字时,变量的值存在于value槽,可以用symbol-value引用。
当符号是词法变量时,其只是个占位符,编译器会将其转为一个寄存器引用或内存的地址。在编译出的代码中无法追踪这个符号(除非使用调试器外加符号表才能进行追踪),只要一有值,符号就消失了。
8 更多与包相关的概念
Package
这个相当于 C++ 里的 namespace,它对应的是符号所处的命名空间,并不对应到文件。package 是在代码运行时才产生的,CL 报错说某个 package 找不到并不是说哪个文件找不到,而是 CL 运行环境
里找不到这个包的定义。
System
一堆文件加一个 CL 版的 Makefile,组合在一起称为一个 system。这里的 "Makefile" 常见的就是 .asd 文件,"make" 对应到 ASDF。这种 "make" 还有 mk:defsystem。
Module
一个代码集合,用 (require 'module-name) or (require :module-name) or (require "module-name") 加载此 CL 实现相关的库,具体哪种形式可行取决于 CL 实现。 有的 CL 实现可以用这个方式加载 ASDF 类型的库。
Library
提供某功能的代码库。