Common Lisp/高级主题/条件系统
Common Lisp 拥有一个高级的条件系统。条件系统允许程序处理异常情况,即程序员定义的程序正常运行之外的情况。异常情况的一个常见例子是错误,但是 Common Lisp 条件系统涵盖的范围远不止错误处理。
条件系统可以分为三个部分:发出或引发条件、处理条件以及提供条件恢复。几乎所有现代编程语言都提供前两种协议,但很少有语言提供最后一种(或区分最后两种)。这种最后一种协议,提供重启或程序恢复的方法,在某种程度上是 Common Lisp 条件处理最重要的方面。
重启是一种从异常情况中恢复的方法。异常情况经常发生,通常是错误。如果你一直在用这本书的 REPL 玩,毫无疑问你至少进入过一次调试器会话。当引发严重条件并且 Lisp 系统别无选择只能询问你该怎么办时,就会调用调试器。它会给你一个可以从这种条件中恢复的方式的选项列表。这些选项就是重启。通常调试器提供的唯一重启是返回到顶层 REPL,但有时你被允许继续或重试计算。此外,你可以定义其他重启。
例如,如果你试图从文件中读取数据,可能会出现很多问题:文件可能不存在,你可能没有足够的权限读取它,或者文件中的数据可能已损坏。这些事件通常被认为是异常情况,并且每种情况都可以用多种方法处理。如果文件不存在,你可能想要指定另一个文件名;如果权限不足,你可能想要指定另一个文件名或修改文件权限使其可读;如果数据已损坏,你可能想要指定一个新的文件名,尝试以有意义的方式解释损坏的数据,甚至尝试修复文件并重新读取。
当你实现重启时,你的工作是识别这些可能的恢复机制并将它们作为条件系统的重启放置到位。为了完成我们的示例,假设你正在读取一个包含多行文件,每行都包含一个 (x,y) 坐标列表,它们只是作为由空格分隔的数字对列出。例如,该文件可能看起来像这样
0 0 100 150 50 30
30 20 65 65 10 20
0 100 150 50 30 0
作为第一步,我们可以这样编写读取该文件的函数
(defun read-points-file (filename)
(iter (for line in-file filename using #'read-line)
(collecting
(iter (for val in-stream (make-string-input-stream line))
(collect val) ))))
现在,我们看看如果我们在不存在的文件上调用该函数会发生什么(在 SBCL 中)
(read-points-file #p"does-not-exist.data")
error opening #P"does-not-exist.data":
No such file or directory
[Condition of type SB-INT:SIMPLE-FILE-ERROR]
Restarts:
0: [RETRY] Retry SLIME REPL evaluation request.
1: [ABORT] Return to SLIME's top level.
2: [TERMINATE-THREAD] Terminate this thread (#<THREAD "repl-thread" RUNNING {BA80BC1}>)
Backtrace:
0: (SB-IMPL::SIMPLE-FILE-PERROR "error opening ~S" #P"does-not-exist.data" 2)
1: ((LABELS SB-IMPL::VANILLA-OPEN-ERROR))
2: (OPEN #P"does-not-exist.data")[:EXTERNAL]
3: (READ-POINTS-FILE #P"does-not-exist.data")
...etc...
我们看到 SBCL 已经引发了一个错误,并且由于它不知道该怎么做,所以它询问用户。它提供了一个重启列表:RETRY、ABORT 和 TERMINATE-THREAD。如果你创建了一个名为does-not-exist.data的文件,那么你可能会使用 RETRY 重启。如果文件存在但由于权限问题而无法读取,我们会得到几乎相同的结果,但错误消息权限被拒绝代替。同样,你可以修复文件并调用 RETRY 重启。
如果无法打开文件进行读取,似乎用户可能输入了不正确的文件名。考虑到这一点,让我们提供一个重启来更改我们尝试打开的文件名。我们可以使用以下形式restart-case或更通用的restart-bind.
(defun prompt-for-new-file ()
(list (prompt "Input new file name: ")) )
(defun read-points-file (filename)
(restart-case
(iter (for line in-file filename using #'read-line)
(collecting
(iter (for val in-stream (make-string-input-stream line))
(collect val) )))
(try-different-file (filename)
:interactive prompt-for-new-file
(read-points-file filename) )))
现在,如果我们因为无法读取文件而进入调试器,我们会得到一个额外的选项:TRY-DIFFERENT-FILE。
(read-points-file #p"does-not-exist.data")
error opening #P"does-not-exist.data":
No such file or directory
[Condition of type SB-INT:SIMPLE-FILE-ERROR]
Restarts:
0: [TRY-DIFFERENT-FILE] TRY-DIFFERENT-FILE
1: [RETRY] Retry SLIME REPL evaluation request.
2: [ABORT] Return to SLIME's top level.
3: [TERMINATE-THREAD] Terminate this thread (#<THREAD "repl-thread" RUNNING {BA80BC1}>)
Backtrace:
0: (SB-IMPL::SIMPLE-FILE-PERROR "error opening ~S" #P"does-not-exist.data" 2)
1: ((LABELS SB-IMPL::VANILLA-OPEN-ERROR))
2: (OPEN #P"does-not-exist.data")[:EXTERNAL]
3: (READ-POINTS-FILE #P"does-not-exist.data")
...etc...
Input new file name: #p"does-exist.data"
==> ((0 0 100 150 50 30) (30 20 65 65 10 20) (0 100 150 50 30 0))
形式restart-case在重启生效的环境中运行其第一个参数。这意味着指定的重启 TRY-DIFFERENT-FILE 可以随时在该第一个形式执行期间被调用。在我们的示例中,在形式执行期间调用了调试器,这意味着重启包含在它可以采取的操作列表中。
条件用于描述程序员不想在主程序流中处理的情况。我们还没有在我们的示例中定义任何条件,但是,一个条件被发出。这个条件是一个错误,open在尝试打开它无法打开的文件时引发。该错误的类型为sb-int:simple-file-error,它内置于 Lisp 系统中。
你可以使用define-condition宏定义你自己的条件,它与defclass宏非常相似。
处理程序的作用是将条件与重启绑定在一起。这意味着如果该条件被引发,该重启将自动被选中,这意味着我们不会被转储到调试器中。
实例化处理程序的形式是handler-case和更通用的handler-bind.
Common Lisp 还提供了一套错误处理机制,这些机制模仿了大多数其他语言中发现的行为。