状况系统 与 异常处理

“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

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

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

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

No.24 登上八达岭长城

某天早晨,与 @刘超程序员 一起做了一个非常临时的决定,去爬长城,于是就去了。

常说不登长城非好汉,这回终于好汉了一把。现在是旅游淡季,外面寒风呼呼地吹,神经里如果没点特殊构造,估计也不会选择这个时候去。冬天坐个长途车也是印象深刻啊,虽然也是三个位子,三个人在圆鼓鼓的衣服包裹之下,感觉上只有两个半位了。

还有一段路程才到入口,路上就能看见山头上镶嵌着疑似长城的物体,果然,长啊。

天色不错,八达岭上方的空气好像比市区要清新许多。
八达岭入口前广场的树

一进来,发现竟然有两边可以走,右边人多,左边人少。我们走左边,面朝太阳前进。登上烽火台,环顾四周,几百年前的边防战士就是在这里执行他们的神圣使命,以长城为界,一边是不知道脑子里装着什么的亡命之徒,一边是需要保护的族人,而现在,触摸着刻满名字的砖墙,感觉太割裂了,只怪那些在上面刻名字的人,以为自己都是齐天大圣。
鸟与长城 继续阅读“No.24 登上八达岭长城”

控制浏览器的缓存

做前端开发时常要重复一个动作,改一点,刷新页面看看效果。这里,我们是有个假设,刷新之后是能看到新的内容的。

但事实上,并不一定。

因为浏览器有缓存的机制。当然,它不是任何情况都起效,如果响应头里面没有 Last-Modified, Expires, ETag 这些东西存在,浏览器也是不知道该怎么做缓存的。那就有办法了,开发的时候控制都不输出它们问题不就解决了吗?如果控制这个的开关只有一处的话,那当然可以,但实际这些响应头的处理是有必要做些细分处理的。如果可以让服务器保持正常一致的输出,那么控制权就落在浏览器哪里了。

控制浏览器的缓存,通常想到的都是清除缓存,所有浏览器都有这样的功能,简单点几下就能搞定。但其实还有更省力的办法,就是直接控制是否缓存。开发通常都喜欢使用的 firefox 上是控制缓存方法很简单,地址栏输入 about:config (会让你确认一下),然后在 filter 里输入 cache
从过滤结果可以找到 browser.cache.disk.enablebrowser.cache.memory.enable 这两项,默认都是 true,双击一下变成 false,缓存功能就禁止了。

这样,一开始的假设就是正确的了,可以放心的 F5 。

其它浏览器呢?

Chrome 据说也可以设置 –disk-cache-size=N,看上去可以控制缓存,不过我在 ubuntu 的 chrome13 上尝试是无效的,不知道 win 下可不可以。Chrome 可以通过 about:cache 查看缓存内容,也是个很有用的工具。

IE 里面是可以设定每次访问都从服务器请求资源。

不过,启用缓存的情况做测试也是必须的。之前就遇到一个非常特殊的例子,Expries 设置不当,禁用缓存情况下测试一切正常,启用的时候就不正常了,F12 一看是 304,清除多余的响应头即可。

由此,可以得出一个结论,服务器资源是可以从缓存角度做区分,适用缓存的部分,为了测试方便可以禁用缓存,而不适用缓存的部分是需要启用浏览器缓存机制进行测试,确保结果符合预期。

写正字

突然脑海中浮现的画面,记录一下

总感觉现在正字用得太少了

曾经是个绝佳的计数工具,因为太容易了,正好五笔,一看就明白,而且不容易混淆
一横一竖一横一竖一横的,多一笔少一笔都不对,毫不含糊,
而且还有长有短的一起搭配,太长就过了,太短也不能凑合
因此它绝对是个专业的视觉计数工具。

IE 中图片大小的误判

IE8 以下的浏览器都会出现这个问题,不致命,但非常影响页面观感。幸运的是,你不一定能遇上。

情景是,图片需要动态加载,根据给出的缩放比例调整显示大小。图片是普通 PNG,只是存放点是多样化的,同样的图片,一个以文件形式存在,另一个是保存于缓存(内存)中,前面都是用 Apache,一个读文件返回,一个做转发,用 IE 访问它们,获取其大小信息,结果竟然是不一样的。

装载图片的代码片段:

function loadImg(url, callback) {
    var img = new Image();
    img.src = url;
    
    if (img.complete) {
        console.log("complete");
        return callback.call(img, url);
    }
    img.onload = function() {
        img.onload = null;
        callback.call(img, url);
    }
    return img;
}

要完成缩放,需要获取图片的原始大小。当然也可以用百分比数值,方案比较不是本文重点。下面写一个函数可以打印出图像对象上所有的数值属性,当然包括宽和高。

function getParam() {
    var arr = [];
    for(var i in this) {
        try {
            if (typeof this[i] !== 'number') continue;
            arr.push(i + ' : ' + this[i] + '\n');
        } catch(e) {} // this[i] 在 IE 下有可能会报错,不影响观察,暂且忽略
    }
    alert(arr.join('')); // 使用 alert 方便观看结果
};

然后都执行下面的,其中 testPngURL 分别为两者的 URL,这也是唯一的区别了。

preload(testPngURL, getParam);

两次执行的结果显示的 width 和 height 是不一样的,而且不成比例,而且结果是可以稳定重复的。我最纳闷的是 IE 凭什么得出另外那个错误结果的。我初时觉得可能是两次请求的 header 不一样,主要差异是存放于内存的,其响应的 Content-Type 是 application/octet-stream,文件形式的则是 image/png,内容长度是完全一样的。于是我使用 Fiddler 对内存图像的请求进行拦截,并修改其 Content-Type,结果是无效的。甚至将两者的响应头弄成完全一致,也得不到一致的结果。

暂时还没想到有什么方法可以绕过。而且最奇怪的是,有少数几个 png 的大小是正确的。难道问题出在 png 的数据上,有些隐藏的信息,但触发点到底是什么,可是敲爆头也不知道了。

虽然生产不会这么使用,但为了开发调试方便,我们会使用缓存来存放各种资源,其返回头都是非常简单而且统一的,无任何多余 header,浏览器不会自作主张做缓存。更新也方便,往同一个地址 PUT 内容即可。现在由于 IE 存在这样一个问题,其显示大小是不正确的,非常影响观感,不得不需要做些调整了。

Linux 20 周年纪念活动在苏州

2011年8月28日 星期日下午,我第一次参加 linux 周年聚会。这次是 linux 20 周年,网上已经煲得比较多有什么历史渊源,特别有纪念意义之类的文字,让人无比期待这次活动。

我是个 linux 爱好者,看好的并不是 linux OS 本身,而是这种社区驱动的发展模式,潜藏在背后的伟大理想,由此而衍生出来的就不仅仅是一个操作系统,而是一套生态系统,时刻启发着参与者,给予丰富多彩的希望。通过这次活动,我不知道大家是不是能感觉到希望就在眼前了。

回到会场内容上。由于小小的误会,迟到了一点。一位身穿 linux20 纪念 T-shirt 的演讲者已经开始了,讲Intel 的历史包袱,Atom 和 ARM 的前世今生,Intel 与 AMD、Nvidia 打得火热的日子,中间还间插了许多名人轶事,N 个名词解释,科普了 Intel 芯片制片工艺(保持领先六个月),冷笑话 N 个,还推荐了一本书,后面终于扯到 linux 了,介绍了 Intel 怎么和软件厂商合作,介绍了大神和来自 http://www.lesswatts.org/ 等地方的可以省电的工具和技术。前前后后两页 PPT,有才吧。

揸水之后,上场的是来自龙芯的童鞋。我一开始不怎么喜欢,他嘴里冒的词都很官腔,讲龙芯的历史也不知道是真是假,但奇怪的是,他不忌讳,也没有避重就轻,兜圈子,那段历史再愚蠢再不堪,依然是历史。只要 继续阅读“Linux 20 周年纪念活动在苏州”

Maven build 中启动 JMX Agent

为了监控 JVM,研究了下 VisualVM,但是不知道怎么导出里面的数据,可导出就比较方便做数据比对,而且也不知道怎么控制显示的历史信息跨度。而这两点在 JConsole 中都可以。于是要折腾 JConsole,一开始还不怎么顺利,虽然看上去只是几项配置。
因为在开发过程当中都是使用 mvn jetty:run 启动的。原来以为是这样:

mvn jetty:run -Dcom.sun.management.jmxremote.port=9410 \
    -Dcom.sun.management.jmxremote.authenticate=false \
    -Dcom.sun.management.jmxremote.ssl=false

然后使用 jconsole 127.0.0.1:9410 ,提示是 Connection refused

后来了解到,如果 JMX Agent 启动成功的话,会产生一个端口监听。
再执行 netstat -a | grep 9410 ,毫无反应。

网上乱 google jconsole, jmx agent, jmx remote, jmx connection refused 等关键字都没什么头绪。
找到一篇 关于 jconsole 的错误排查 的不错的文章,但是情况不吻合,这里唯一区别的是不是直接 java 启动,而是通过 mvn ,曾经使用过调试,需要 继续阅读“Maven build 中启动 JMX Agent”