Skip to content

LISP - 包

Lisp - 用于代码组织的包(Packages)

Section titled “Lisp - 用于代码组织的包(Packages)”

随着程序的增长,您不可避免地会遇到函数、变量和类共享相同名称的情况。为了防止这些名称冲突,Lisp 提供了一种强大的命名空间机制,称为 包(packages)。包是一个符号表,它将名称(字符串)映射到符号。

可以把它想象成您代码名称的目录。my-function 在 package-a 中的符号与 my-function 在 package-b 中的符号是完全不同的对象。

当前包存储在全局变量 *package* 中。默认情况下,您通常从 COMMON-LISP-USER 包开始。

创建包的现代、声明式方法是使用 defpackage 宏。最佳实践是将所有 defpackage 形式放在一个单独的文件中,通常命名为 package.lisp,并在加载任何其他代码之前加载它。

defpackage 宏有几个重要的选项:

  • :use:指定要从哪些其他包继承符号。几乎总是包含 :common-lisp。
  • :export:定义包的 公共 API 的符号列表。这些符号可以在其他包中直接访问,无需包前缀。
  • :import-from:将特定符号从另一个包导入到当前包的命名空间中。
  • :nicknames:包的更短、替代名称列表。

让我们创建一个简单的计算器小项目,看看包在实践中如何工作。我们将包含两个文件:package.lisp 和 main.lisp。

;; package.lisp 文件
(defpackage #:my-calculator
(:use #:common-lisp)
(:export #:add #:subtract))

在这里,我们定义了一个名为 my-calculator 的包。它使用了 common-lisp 中的所有标准符号,并导出了两个符号:add 和 subtract。这些是我们希望程序的其他部分(或其他人)使用的公共函数。

;; main.lisp 文件
(in-package #:my-calculator)
;; 这是一个内部的、未导出的辅助函数。
;; 其他包无法轻易访问它。
(defun log-operation (op x y)
(format t "Performing ~a on ~a and ~a~%" op x y))
;; 这是一个导出的函数,属于我们的公共 API 的一部分。
(defun add (x y)
(log-operation 'add x y)
(+ x y))
;; 这是另一个导出的函数。
(defun subtract (x y)
(log-operation 'subtract x y)
(- x y))

顶部的 (in-package ...) 声明会切换当前包。之后的所有 defun 形式都将在 my-calculator 包内创建符号。

现在,让我们看看如何使用我们新的 my-calculator 包。想象这可能是您应用程序的不同部分,或者您正在 REPL(读取-求值-打印循环)中使用它。

;; 假设上述文件已加载。
;; 要使用我们的包,我们可以通过包名限定符号名。
(print (my-calculator:add 10 5))
;; 我们也可以在另一个包定义中使用 :use 选项,
;; 或者在交互式环境中使用 `use-package` 函数。
(use-package '#:my-calculator)
;; 现在导出的符号可以直接使用。
(print (subtract 10 5))
;; 尝试访问内部符号将导致错误或警告。
;; 双冒号 `::` 用于访问内部符号,但这
;; 强烈不推荐,因为它破坏了封装性。
;; (my-calculator::log-operation 'test 1 2) ; <-- 不要这样做!
Performing ADD on 10 and 5
15
Performing SUBTRACT on 10 and 5
5

在实际的 Common Lisp 项目中,您不会手动加载文件。您会使用 ASDF(Another System Definition Facility,另一个系统定义工具) 将您的项目定义为“系统”。一个 .asd 文件告诉 Lisp 哪些文件属于您的系统及其依赖关系。

此外,Common Lisp 事实上的包管理器是 Quicklisp。它允许您通过一个命令(例如 (ql:quickload :library-name))下载和管理数百个开源库。掌握包、ASDF 和 Quicklisp 是现代专业 Common Lisp 开发的基础。