Common Lisp 交互式开发
lisp (load (expand-file-name "~/quicklisp/slime-helper.el"))
4. 启动 Emacs,执行 `M-x slime`,就会启动 SBCL 并进入 SLIME 环境。
### 2.2 SLIME 的基本操作
| 快捷键 | 功能 |
|--------|------|
| `C-c C-c` | 编译当前顶层表单(defun 等) |
| `C-c C-k` | 编译当前文件 |
| `C-c C-d d` | 查看光标下符号的文档 |
| `C-c C-m` | 在 REPL 中展开宏 |
| `M-.` | 跳转到符号的定义 |
| `C-c C-z` | 切换到 REPL 缓冲区 |
## 3. 核心交互工作流
### 3.1 在 REPL 中探索与定义
启动 SLIME 后,`*slime-repl*` 缓冲区就是你与运行中 Lisp 交互的地方。
```lisp
CL-USER> (+ 2 3) ; 简单求值
5
CL-USER> (defun greet (name)
(format nil "Hello, ~a!" name))
GREET
CL-USER> (greet "Alice")
"Hello, Alice!"
诀窍:不要直接在 REPL 中编写大段代码。推荐在另一个 Emacs 缓冲区中打开 foo.lisp 文件编写代码,然后使用 C-c C-c 将定义编译到镜像中。这样既保留历史又方便协同。
3.2 增量修改函数
假设你定义了如下函数:
(defun add-tax (price)
(* price 0.08))
运行后觉得税率应改为 0.07,只需修改代码并再次将 defun 编译到镜像(光标放在 defun 内按 C-c C-c)。所有已经调用 add-tax 的函数在下次调用时就会使用新定义,无需重编译它们。这就是运行时的无缝热替换。
3.3 使用 * 变量和历史
Lisp REPL 保存了最近的求值结果。
*最后一个值**倒数第二个值***倒数第三个值+,++,+++分别对应最后的输入表单
示例:
CL-USER> (mapcar #'1+ '(1 2 3))
(2 3 4)
CL-USER> (reverse *)
(4 3 2)
4. 编译与调试的即时循环
4.1 编译警告与错误的处理
如果你编译一个包含错误的函数,SLIME 会高亮错误位置并给出具体信息。你可以修改代码,再次编译,错误即消除。无需重启,无需重新设置环境。
(defun bad-fn (x)
(+ x y)) ; y 未定义
编译时 SBCL 会给出 The variable Y is unbound 警告。修复后重编译即可。
4.2 运行时错误与交互式重启
当运行中发生错误时,Lisp 会进入调试器,而不是崩溃。调试器提供了多种**重启(restart)**选项,例如:
RETRY:重新执行出错的操作ABORT:回到上层USE-VALUE:提供一个新值继续计算
你可以在出错时修改变量值,或者用新代码替换出错函数,然后选择重启继续执行。这种能力在调试长时间运行的服务时无与伦比。
(defun divide (a b) (/ a b))
(divide 10 0) ; 触发除以零错误
在调试器里,你可以选择 RETRY 但会再次失败;也可以先定义一个新除法函数,然后重新加载,再选择 RETRY,程序就能继续。
4.3 检查运行时状态
使用 describe、inspect 快速探查对象结构。
CL-USER> (describe 'divide)
DIVIDE
[symbol]
DIVIDE names a compiled function:
Lambda-list: (A B)
Derived type: (FUNCTION (T T) (VALUES NUMBER &OPTIONAL))
Source file: .../example.lisp
5. 高级交互技术:热重载与面向对象
5.1 修改类(CLOS)定义
如果你使用 CLOS 定义类,并在运行中的系统里修改类结构,Lisp 可以更新已有的实例。
(defclass person ()
((name :initarg :name :accessor name)))
(defvar *alice* (make-instance 'person :name "Alice"))
现在修改 person 类增加一个 age 槽:
(defclass person ()
((name :initarg :name :accessor name)
(age :initarg :age :initform 0 :accessor age)))
*alice* 会自动获得一个初始值为 0 的 age 槽,无需重新创建。可以通过 (update-instance-for-redefined-class) 标准方法自定义更新逻辑。
5.2 包管理中的动态扩展
由于 Common Lisp 的包系统,你可以在运行时加载新系统或库。例如,用 Quicklisp 加载 cl-json 来解析 JSON:
(ql:quickload :cl-json)
加载后即刻可用,无需停止你的应用。
6. 实际场景演练:构建一个简单的 Socket 服务器
让我们演示一个不断运行的服务器,我们可以在不停止它的情况下修改其行为。
- 启动服务器(在文件中编写并编译):
(ql:quickload :usocket)
(defun start-server (port handler)
(let ((socket (usocket:socket-listen "127.0.0.1" port :reuse-address t)))
(format t "Server started on port ~a~%" port)
(loop
(let ((conn (usocket:socket-accept socket)))
(funcall handler conn)))))
(defun echo-handler (stream)
(let ((line (read-line (usocket:socket-stream stream) nil)))
(when line
(format (usocket:socket-stream stream) "Echo: ~a~%" line)
(force-output (usocket:socket-stream stream))))
(usocket:socket-close stream))
在 REPL 中执行:
;; 在一个新线程中启动,避免阻塞 REPL
(defvar *server-thread*
(bt:make-thread (lambda () (start-server 8080 'echo-handler))))
- 运行时修改处理函数:
现在服务器正在运行。我们可以重新定义
echo-handler以返回大写内容:
(defun echo-handler (stream)
(let ((line (read-line (usocket:socket-stream stream) nil)))
(when line
(format (usocket:socket-stream stream) "Echo (upper): ~a~%" (string-upcase line))
(force-output (usocket:socket-stream stream))))
(usocket:socket-close stream))
编译这个定义 (C-c C-c)。新连接会立即使用新处理函数,旧连接不受影响。因为 start-server 通过符号 'echo-handler 每次调用,所以重新定义会自动生效。
- 关闭服务器(随时可做):
(bt:destroy-thread *server-thread*)
整个过程服务器从未停止接收连接,你只是在一边写代码,一边编译,一边看到效果。
7. 常见问题与最佳实践
7.1 状态管理
因为镜像会累积状态,要有意识地管理动态变量和全局状态。尽量使用 defparameter、defvar 并在需要时重置。使用 make-package 隔离命名空间。
7.2 持久化与恢复
交互式开发并不意味着丢失环境。很多项目使用 core 文件保存完整镜像,以便第二天恢复。SBCL 支持保存镜像:
(sb-ext:save-lisp-and-die "myapp.core" :toplevel #'main :executable t)