状况系统 与 异常处理

“Lisp 的状况系统(condition system)是它最伟大的特性之一。”

有没有最伟大我还不是很清楚,但可以肯定的是给了我耳目一新的感觉。

会 Java 的应该都对了解其异常处理的机制,这一类型的异常处理方式估计是使用最广泛(Python, JavaScript, etc)的了。
其表达形式也比较简单:(以 Java 为例)

try {
  // do something could cause exception
} catch (Exception e) {
  // handle exception
}

而且,产生的异常如果不好处理还可以往上抛(throw exception),当异常向上层抛出的时候,异常信息以异常对象的形式向上传递,调用栈会展开(unwind),如果某一层没 catch 住或者处理不了继续往上抛。相当不错的模型,简洁易用。

似乎一切异常都逃不出手掌心了。到现实中的代码翻翻看。先排除掉哪些 catch 块里面没有任何东西,或者不写 try catch 直接声明 throw 的情况,因为不使用就没有发言权。可以观察到下面的情况出现得相当的多。

e.printStackTrace();
throw e;

顿时有种泪流满面的感觉,教科书上的内容还记得 T_T
好吧,那证明不了什么,只是说明了水平不够,没能领悟异常处理的真谛。

假设我们已经在 catch 里花了许多心思处理异常,异常只有一种处理方式的时候还好,一旦有多种处理方式,在什么情况下选择怎么处理就变得棘手起来,由于我们尽量期望一个方法只做它自己事情,选择哪个处理方式又是很可能与上层调用环境有关,要么是在调用时一并传入更多上下文信息,要么是抛出异常,由外层去选择和处理,只是怎么处理异常情况应该是下层的方法了解得最清楚,一旦异常抛出,栈已经被展开,前缘难续。当然,模型本身是没有什么对错的,只是看怎么用,用起来是否费劲。

从概念上比较,Lisp 的状况系统理解起来是有点费劲的,但用法却比较简单。貌似它的一些其它特性也是这样子。

状况系统将整个过程分成:产生(signaling)状况,处理(handling)它以及再启动(restarting)。与突出异常的情况不同,Lisp 更倾向于处理并从错误状况中恢复。但是,底层函数中是可以不决定选择哪种恢复策略,将决定权留给高层函数的代码。我们要做的事情就是思考会出现什么状况,出现状况的时候有什么策略来应对,以使程序可以继续运行下去。这要求比简单捕捉或抛出异常要更进了一步。

下面 Lisp 代码引用自 《实用 Common Lisp 编程》

(define-condition malformed-log-entry-error (error)
  ((text :initarg :text :reader text)))

(defun skip-log-entry (c)
  (invoke-restart 'skip-log-entry))

;; set up the conditions
(defun parse-log-entry (text)
  (if (well-formed-log-entry-p text)
    (make-instance 'log-entry …)
    (restart-case (error 'malformed-log-entry-error :text text)
      (use-value (value) value)
      (reparse-entry (fixed-text) (parse-log-entry fixed-text)))))

;; 'skip-log-entry' will be shown in the debugger when error occur
(defun parse-log-file (file)
  (with-open-file (in file :direction :input)
    (loop for text = (read-line in nil nil) while text
       for entry = (restart-case (parse-log-entry text)
                     (skip-log-entry () nil))
       when entry collect it)))

(defun analyze-log (log)
  (dolist (entry (parse-log-file log))
    (analyze-entry entry)))

;; handle the condition
(defun log-analyzer ()
  (handler-bind ((malformed-log-entry-error
                  #'(lambda (c)
                      (use-value
                       (make-instance 'malformed-log-entry :text (text c))))))
    (dolist (log (find-all-logs))
      (analyze-log log))))

调用是从 log-analyzer > analyze-log > parse-log-file > parse-log-entry 逐层深入。在 parse-log-entry 产生状况(malformed-log-entry-error)。然后可以使用 HANDLER-CASE 宏来定义状况处理器(handler),以响应状况的信号处理,但如果只是这样的话,就和普通异常处理没什么区别。

状况系统允许我们将处理代码拆分成两部分:从错误中恢复用的再启动(restart),和状况处理器(handler)。前者可以位于较下层的函数,后者位于更上层。

Restart 提供了可选的处理策略,因为并不是所有的应用都会采取某种固定的处理方式。比如,例子中 parse-log-entry 提供了 use-value 和 reparse-entry 两种处理方式,可以直接提供一个值,或者修复数据后重新分析。多个处理策略有时是很有用甚至是必须的。应用需要运行在测试环境与生产环境等不同环境中时,就有许多需要不同的处理策略的情况。

log-analyzer 是顶层函数,函数内通过 HANDLER-BIND 宏绑定状况处理器。重点在于,当下层函数出现状况时,上层函数可以选择处理策略,而执行处理策略的还是在下层函数的调用栈内,因为整个过程里栈是不展开的,可以在原有上下文环境下继续进行处理。

某种意义上讲,状况系统与异常处理根本就不是一回事。

我感觉就是两种完全不同的处理方式。当你与一个朋友因为一些状况发生争执时,如果这事在两人之间能处理掉那是最好,处理不了或者无法决定处理办法,拼命地往上冲突,朋友关系可能就因此破裂了,就算后面再努力挽回,重新建立的已经不是以前的那份友情了。但如果只要有修复的可能,上层有比如珍惜彼此多年情谊这样的大前提,是能决出一种处理办法来修复,从而很好地延续了友情。

一切都在继续。

PS: 初学 Lisp,实践不多,如果理解得不正确,还望看官们多多提点。

CLisp 中含非零小数位的数值比较问题

在试验 CLisp 的数值比较的时候,发现一个奇怪的东西

CL-USER > (= 0.2 1/5)
NIL

CL-USER > (> 0.2 1/5)
T

CL-USER > (< 0.2 1/5)
NIL

但是,当不涉及到小数的时候是一切正常的

CL-USER > (= 1.0 1)
T

CL-USER > (= 1.0 10/10)
T

而且,带小数总是大于用分数形式表达的。从形式上很难看出来是什么问题。

“函数 = 是用数学意义上的值来比较数字,而忽略类型上的区别。”

这么说,就是带小数的浮点数的数学意义值比理论上等值的分数的要大。

开始学习 Lisp

好吧,我是受到 《黑客与画家》 的催眠,去看 LISP 的,不过越看越清醒。

真的是好东西。之前看 Scala 的时候惊叹于它的 表达式 与 函数 之间的模糊边界,而且函数式编程使我从执着于模仿 Java 的编程方式中释放出来,对应该怎么样写代码有了更多新认识。虽然我了解的支持 FP 的首个语言不是 scala,是 JavaScript,只是很少人会把 FP 当成是一种特色,一种解决问题的方式。

学会了语法,了解了库,学会了设计模式,是能够让我开始编写代码解决问题了,但是不需要很长时间,就会发现“砖头”(构成代码的单元)不够用了。也就是自造砖头会成为一种常态,而怎么自制出跟你已经用惯了的那些基础砖头一样好用的砖头就成为一个问题——如何更好地表达问题,更好地抽象。而这个问题解决不好,它会以各种各样形式表现出来,我觉得是使我经常郁闷的重要来源。而且代码不仅仅是给自己用的,通常是一个开发小组里使用,它就成了像流行性感冒一样的不治之症,不时发作。在被各种头疼弄死之前,我觉得我还可以再抢救一下。

不是说 Lisp 本身多么的强大,也不是想要比较各种语言的强弱,程序猿要的不就是能够用好来表达自己解决问题的方式的语言么。其实我也说不准什么时候才会实战 Lisp ,更何况现在正受到 Nodejs 的感染……
但这个开始,给了我信心,带来了更多可能性。

传送门:
Lisp 之根源 (中文的,还有惊喜)
ANSI Common Lisp (第一、二章,也有中文的)