Archive

Archive for the ‘Lisp’ Category

Using infix notation in Common Lisp

February 24th, 2008 2 comments

以前在用 Common Lisp 做计算方法的作业的时候就觉得 Common Lisp 的 prefix notation 对数学表达式不够友好。最近打算用 Common Lisp 实现些算法,再次遇到了这个问题。好在已经有人做了将 infix notation 转成 prefix notation 的工作,所以我就可以用现成的了。

CMU 的 AI repository 里有一个 INFIX: Infix reader macro for Common Lisp,实现了一个 reader macro,用它就可以在 Common Lisp 里面使用 infix notation 了,不过需要把表达式用 #I() 括起来。

代码注释里面有一些例子,也有使用说明,看看就能用了。这里举几个简单的例子:

#I(1 + 2) ; 3
#I(2^^2 + 4) ; 8
#I(1 == 2) ; 判断, 得到 nil
#I(1 < 2 < 3) ; T

有了这个 read macro 数学表达式的输入就简单多了,布尔表达式也可以用类似 C 的语法来写,不用再做人肉 infix -> prefix 转换器了。

除此以外,它还提供了函数调用数组访问的特殊语法,而且用 = 号做赋值,同样是用了类似 C 的语法。

(defparameter v (vector 0 1 2 3))
#I(v[1]) ; 1
#I(v[0+1] = -1) ; 现在 vec 中的第二个元素值为 -1
#I(random(5)) ; 调用函数 random

我比较喜欢用这种语法来访问数组元素,比用 aref 看起来更清楚些,还能少打些字。

如果想知道这个 read macro 在背后究竟做了什么的话可以在 #I() 之前加个单引号。

'#I(v[0+1] = -1) ; (SETF (AREF VEC (+ 0 1)) 1)

跟 Python, Ruby 这样的语言比起来 Common Lisp 的程序代码并不是很紧凑,因为有很多长的函数名,一些常用的操作也没有特殊的语法来支持。只使用 prefix notation 的好处就是可以方便的使用宏,更容易的进行元编程,也使得程序员可以自己定义语法,这里的 infix read macro 就是一个例子。

Paul Graham 在他的 Lisp 方言 Arc 中就希望能够使 Lisp 的代码更紧凑,更短,添加一些语法来简化某些操作。通常来说程序中的 bug 数量与程序的长短成正比,与使用的语言无关,如果能写出来的程序更短那么错误应该也会更少。不知道以后 Arc 的发展会如何,不过现在就已经有一个对 Lisp 语法做出许多改动的基于 Common Lisp 的语言,Qi。Qi 的作者正准备大幅改进语法,推出 Qi II,等出来了再去看看,到时候看是否可以把它和 Common Lisp 混合使用。

Tags:

LispCast — more Lisp Movies

November 6th, 2007 No comments

又有热心的 Lisp 程序员制作开发录像了,LispCast 这个 blog 将发布一系列的 Common Lisp 开发录像,作者也将发布一些 Common Lisp 相关的文章。这篇 post 是帮作者宣传一下,也算是对这个项目的一点贡献吧。

从 about 页面里抄段介绍来:

  1. To enhance the code quality and clarity of existing Lisp programmers.
  1. To introduce Common Lisp to those programmers who are interested.
  1. To gain acceptance of Common Lisp as a modern, relevant language in the programming community.
Tags:

SLIME movie

August 28th, 2007 No comments

使用 SLIME 的极好的教学电影,制做人 Marco Baringer 。电影介绍了使用 SLIME 进行代码编辑,跟踪函数调用,调试、测试,用 asdf-install 安装库,查看文档等许多方面,同时也可以看看 Lisp 程序员是怎么写代码的。(这样的 IDE 用起来真的是很爽!)

Bill Clementson 的 Blog 找到的。给个下载链接吧。

http://common-lisp.net/movies/slime.mov

http://common-lisp.net/movies/slime.torrent

(这两位都是 Mac 的用户,那个电影里面桌面就是 Mac,Lisp 用的是跑在 Linux 机器上的 SBCL,通过 ssh 连接过去的。Macro 另外一个关于 UCW 的电影则直接用的 Mac 上的 OpenMCL。)

Macro Baringer,他是 UnCommon Web 的作者, UCW 是一个基于 continuation 的 Web 开发框架。这样的 Web 框架最成功的应该是 Seaside 了,使用的语言是 Smalltalk。我目前对 Web 开发还没多少经验,所以看他们介绍使用 continuation 进行 Web 开发的好处没有概念,就不多罗嗦了。

Tags:

SLIME & encoding

August 19th, 2007 No comments

While I was experimenting “cating” a file in Common Lisp, I found that SLIME will report lost connection once I read an UTF-8 encoded file. For plain ascii file, however, there’s no problem. I tried to run the program directly in SBCL, it also worked fine. So I was sure that this problem was caused by SLIME.

After Googleing for a while, I came to this soluiton. Just put this in your .emacs file.

(require 'slime)
(setq slime-net-coding-system 'utf-8-unix) ; Add this line.
(slime-setup)

The setq tells SLIME to use the specified encoding. And now reading UTF-8 encoded file has no problem, and you can also input Chinese characters into SLIME.

For more information, follow this thread in the slime-devel mailing list.

Tags:

用 ASDF-INSTALL 自带下载安装 Common Lisp 库

August 12th, 2007 No comments

以前下载安装 Common Lisp 的库都是手工进行的,其实 ASDF 有个扩展 ASDF-INSTALL 可以自动从网上下载并安装 Common Lisp 的库,而且可以自动解决依赖性问题。

首先最好安装 gpg,安装过程中会通过 gpg 来进行验证。当然你可以选择跳过检查,但是一定要装上,否则安装过程中会因为没有 gpg 而出错。

对 SBCL 的话只需要

(require 'asdf-install)

就可以开始使用了。比如要安装 memoize

(asdf-install:install 'memorize)

ASDF-INSTALL 会首先问你安装到系统还是你的主目录下,之后它自动从网上找到这个库文件,然后下载、安装。安装过程中会提示是否进行验证,编译出错如何处理等问题。完成之后就一切 OK 了。

如果安装在主目录下的话,包文件是放在 ~/.sbcl/site 目录下,所有的 asd 文件都在 ~/.sbcl/systems 目录下建好软链接,把这个 systems 目录设置到 ASDF 的 central-registry 里就可以很方便的使用了。

现在有一个问题,在我这里 ASDF-INSTALL 对有些库并没有自动解决依赖性,不知道为什么。

Tags:

right fold vs. left fold

August 11th, 2007 No comments

Why functional programming matters(非常推荐这篇论文。) 时看到它给出的 reduce 与 common lisp 中提供的 reduce 不同。

这篇论文里面的 reduce 作用是这样的,对一个 list

(cons e1 (cons e2 (cons e3 ( ... (cons en nil)))))

reduce 接受两个参数,一个是接受两个参数的函数,把这个函数去替换 list 中所有的 cons 函数;另一个参数则替换 nil。举例来说,如果这样调用 (reduce add 0 list),则产生的效果是

(add e1 (add e2 (add e3 ( ... (add en 0)))))

也就是对 list 中的元素进行了求和。

这个 reduce 非常强大,可以用它方便的写出 append, map 等函数。(append a b) 即 (reduce cons b a),(map func list) 即

(defun map (fun list)
  (foldr
   (lambda (a b)
     (cons (funcall fun a) b))
   nil
   list))

因为 Common Lisp 中没有函数组合操作符,所以这个 map 没有在论文使用的语言 Miranda 那么优雅。

Common Lisp 中的 reduce 与上面描述的不太一样,初值要通过 key 参数 :initial-value 指定,写成这样 (reduce #’add list :initial-value 0),而且它的默认行为也不太一样。一番搜索之后发现了这个操作通常称为 fold,Wikipedia 上 fold 条目 对这个操作有详细的解释,而且解释 fold 操作的两张图片也很直观。

fold 操作最方便的地方是使得一个原本只接受有限个参数的函数能够作用在元素个数不定的 list 上。

fold 操作分两种,上面说的那种被称为 right fold,而 Common Lisp 中的 reduce 默认是 left fold,C++ 中的 accumulate,Ruby 中的 inject 都是 left fold。Common Lisp 中的 reduce 加上 key 参数 :from-end t 以后其行为就跟 right fold 相同了。Paul Graham 的 Ansi Common Lisp 的 Appendix D 中对 reduce 的说明也很清楚,下面的例子就参考了他的说明。

对最开始的 list 例子用 left fold 操作以后的效果如下:

(add (add (add ( ... (add 0 e1) e2) e3) ... ) en)

简单来说,left fold 也接受两个参数,一个是接受两个参数的函数,另一个是初值。left fold 将初值和 list 中的第一个元素传给函数,然后将函数返回值和第二个 list 元素传给函数,一直这样下去直到 list 的最后一个元素 nil 之前。left fold 也可以轻易的实现求和等操作,当然对于不满足交换率的操作使用 left fold 和 right fold 得到的结果是不同的。但是要实现 append, map 这样的操作就无能为力了(可能也可以,但肯定很麻烦吧)。

Ansi Common Lisp 第 4 掌的第 2 条习题就是用 reduce 来实现 copy-list 和 reverse,只针对 list,不考虑 array。实现如下,

(defun copy-list (lst)
  (reduce #'cons  lst :from-end t :initial-value nil))

(defun reverse (lst)
  (reduce #'(lambda (a b) (cons b a)) lst :initial-value nil))

第一个用的是 right fold,非常直观,用 cons 替代原来 list 里面的 cons 就可以了,第二个用了 left fold,匿名函数只是交互了一下 cons 的两个参数而已,看下 right fold 的针对 add 的那个例子就很容易明白了。

Fibonacci 数列 续

July 22nd, 2007 No comments

先记录一段流水帐,要不然会不记得自己是怎么找到这些东西的,没兴趣看流水帐的跳过。

我想看看用 Common Lisp 怎么写插入法排序(我对语言本身还不熟悉,所以想看别人怎么写的),于是 Google “common lisp insertion sort”,来到了这里,LiteratePrograms,一个记录了很多程序的 Wiki,很不错的地方。看了一会儿就想也许 Fibonacci 数列的算法也会有,于是 Google “common lisp fibonacci”,这次同样也有 LiteratePrograms 上面的文章出现在结果中,不过位置比较靠后,而且上面的算法也很普通。第三个结果来自 CLiki,文章在这里,这里的算法除了那个 generator 以外我在上一篇文章里面都有给出,包括那两个 Θ(lg(n)) 的算法。而最后在 LiteratePrograms 上逛的时候又看到了一个用矩阵乘法的算法。

利用 generator 来求 Fibonacci 数列

利用 generator 的算法比较特别,有点像 continuation。看过 Wikipedia 的解释以后才知道什么是 generator,而 continuation 可以用来实现 generator。代码如下:

(let ((a 0)
      (b 1))
  (defun fib ()
    (prog1 a (psetf a b b (+ a b)))))

这里的实现使用了 closure,let 返回了一个函数 fib,fib 中使用到的 a, b 在 let 创建的环境中,每一次调用 fib 都会返回 Fibonacci 数列中的下一个值,并且更新环境中的 a, b。prog1 对第一个 form 求值,然后对后面的 form 进行求值,最后返回第一个 form 的值。而 psetf 在赋值之前对所有的需要求值的 form 进行求值,然后在赋值,这样就避免了要使用一个变量来记录 a 的值的需要。

这个算法只是用函数的形式表达迭代而已,除此以外没有什么特别的。但是注意使用 generator 我们相当于得到了一个长度无限的 Fibonacci 数列,我们方便地可以控制迭代的过程,在需要的时候才进行迭代。

关于 closure 再多说两句。这个实现里的 fib 函数实际上带有自己的状态,其状态就是用 let 定义时候的 a,b 变量,注意这两个变量在函数之外是无法访问到的。使用 C++ 当然也可以实现相同的效果,但是你得定义一个类,里面封装这两个状态变量,然后重载括号符,使这个类成为一个 functor,实在是麻烦,有兴趣的可以去 Wikipedia 的 generator 条目里面看看 C++ 的实现。而使用 closure 的话,创建带状态的函数一切都是那么自然,不知道 Java 7 里面最终是否会加入 closure。

Dijkstra 的笔记,In honour of Fibonacci

在 CLiki 上发现我所使用的 Θ(lg(n)) 的算法原来 Dijkstra 也有给出过,在他的一篇名为 In honour of Fibonacci 的笔记中记录着。去看了看他的笔记,原来我用的算法和 Dijkstra 记录下来的一样,不过他在计数的时候从 1 开始而不是从 0 开始。(Dijkstra 是不是也是数学家?)Dijkstra 还把这个关系式泛化了,不过有几处推导过程没有看懂,也不知道泛化以后有什么用。

用矩阵乘法求 Fibonacci 数列

另外在 LiteratePrograms 上看到求 Fibonacci 数列的 Ruby 版本中,有一个使用矩阵的算法。方法是对矩阵 ((1 1) (1 0)) 做 n – 1 次乘方,即可得到 Fibonacci 数列第 n 项。代码如下:

require 'matrix'

FIB_MATRIX = Matrix[[1,1],[1,0]]
def fib(n)
	(FIB_MATRIX**(n-1))[0,0]
end

这个算法的优化就可以交给矩阵乘法去做了,矩阵乘法如果是 Θ(lg(n)) 的时间复杂度那么这个算法也是 Θ(lg(n)) 的时间复杂度了。

后来又到 Wikipedia 上搜了一下,在 Fibonacci number 条目下有关于 Fibonacci 数列的非常之多的数学关系和等式的证明,还列举了非常多的参考资料,包括 Dijkstra 的笔记。矩阵形式在上面也有,而且通过矩阵乘法的性质很容易的推导出了复杂度为 Θ(lg(n)) 算法中利用的等式。众人编辑的力量果然强。

为什么不直接用通式求 Fibonacci 数列的第 n 项

Fibonacci 数列的通式我当然知道,但是直接使用它来求数列的第 n 项在精度上会出现问题。通式中有无理数,因此计算过程中会产生误差。根据 CLiki 上的说明,在 CLISP 2.33.2,32 bit 的机器上,对单精度浮点数,当 n < 32 时,使用 truncate 可以得到正确结果;对双精度浮点数,n < 76 时,使用 rounding 可以得到正确结果。超出这个范围都将在整数级别上出现错误。

除了计算上的问题,我觉得这个问题本身也比较有意思。就像 MIT 的教授在上算法导论时说的,”Speed is fun!”,我也这么觉得。而且,当发现自己做着 Dijkstra 曾经也做过的事情的时候还是会小有成就感的。

求斐波那契 (Fibonacci) 数列第 n 项的算法

July 20th, 2007 7 comments

最近看算法导论的习题时联想到 SICP 的一条习题后想到的,折腾了下,得到了几种算法,不同的算法时间复杂度有 Θ(a^n), Θ(n), Θ(lg(n)) 几种。时间复杂度为 Θ(lg(n)) 的算法 比较有意思。下面一个个说,具体算法用 Common Lisp (CL) 实现,至于为什么用 CL 我在文章最后说明。

约定

符号 ^ 表示乘方运算。F(n) 表示 Fibonacci 数列的第 n 项,n 从 0 开始。由此, Fibonacci 数列可以由如下定义给出:

F(0) = 0
F(1) = 1
F(n) = F(n - 1) + F(n - 2)

这个定义本身是递归的。要是可以用在 WordPress 里面使用 LaTeX 输入数学公式的插件就好了,下文中还会出现 _ 来表示下标,这些习惯跟 LaTeX 中数学公式的输入一样,不过看起来就会比较丑了。

最简单的递归定义和迭代实现

递归算法

根据定义可以很方便的得到一个求 F(n) 的算法:

(defun fibonacci-rec-simple (n)
  (cond ((= n 0) 0)
	((= n 1) 1)
	(t (+ (fibonacci-rec-simple (- n 1))
              (fibonacci-rec-simple (- n 2))))))

defun 定义一个函数,参数是 n,cond 类似于 C 中的 switch,t 表示真,函数体的最后 一个 form 作为返回值返回。另外注意数学表达式都是前缀表达式的形式。就算不懂 CL 应该也能看懂吧。

这个递归的版本应该是最容易写的了,直接翻译定义就行。不过可惜的是这个算法因为太多的重复计算导致时间复杂度非常大,计算 F(n) 时需要计算 F(0), F(1) 共 F(n + 1) 次( 可以用数学归纳法很容易的证明),而 F(n) 是最接近 ((1 + sqrt(5))/2)^n/sqrt(5) 的 整数,所以这时计算 F(n) 的时间复杂度为 Θ(a^n)。在我的笔记本上,当 n = 50 时,我就已经不愿意等待到结果输出了。

迭代算法

如果定义变换 T(a, b) 为:

T(a, b) = (a + b, a)

那么,从 (1, 0) 开始,进行 n 次变换后,序对中的第二个元素就是 F(n) 了。由此可以 得到求 F(n) 的迭代算法:

(defun fibonacci-iter-simple (n)
  (loop repeat (+ n 1)
     for b = 0 then a
     and a = 1 then (+ a b)
     finally (return b)))

这个算法的时间复杂度为 Θ(n),没什么好说的。稍微解释下语法吧,loop 是 CL 中构造循 环的一个宏,原来我是用 do 来进行循环的,但发现用 loop 更容易让人看懂,所以这里就写了个 loop 的版本。

这一段不少内容抄袭了 SCIP 1.2.2 中的分析,尽情鄙视我吧。

对递归定义的变形提高算法性能

抱歉,上面的内容没有什么新意,非原创。下面的应该会有点新意了。

最初的递归定义每次递归都只是将问题规模从 n 变到 n – 1 和 n – 2,我的想法是把递归定义进行变换,使得每次递归能够将问题规模一下子减小许多。因此我不断将 F(n) 进行递归展开,

F(n) = 1 * F(n - 1) + 1 * F(n - 2)
     = 2 * F(n - 2) + 1 * F(n - 3)
     = 3 * F(n - 3) + 2 * F(n - 4)
     = 5 * F(n - 4) + 3 * F(n - 5)
     = 8 * F(n - 5) + 5 * F(n - 6)
     = ...
     = F(p + 1) * F(n - p) + F(p) * F(n - (p + 1))

上面的关系式可以很容易使用数学归纳法证明。很自然的想到希望能够在每一次迭代的时候将问题的规模减小到原来的一半。

当 n 为奇数时,令 p = (n – 1) / 2,则:

F(n) = F((n + 1)/2)^2 + F((n - 1)/2)^2

当 n 为偶数时,令 p = n / 2,则

F(n) = F(n/2 + 1)*F(n/2) + F(n/2)*F(n/2 - 1)
     = F(n/2)^2 + 2*F(n/2 - 1)*F(n/2)

上面两个式子合起来可以写出下面的形式:

F(2*n) = F(n)^2 + 2*F(n)*F(n - 1)
F(2*n + 1) = F(n + 1)^2 + F(n)^2

有了上面的递归定义,可以得到下面的算法:

(defun fibonacci-rec (n)
    (cond ((= n 0) 0)
	  ((= n 1) 1)
	  ((evenp n)
           (let ((tmp (fibonacci-rec (/ n 2))))
             (* tmp (+ tmp (* 2 (fibonacci-rec (- (/ n 2) 1)))))))
	  (t (+ (expt (fibonacci-rec (/ (+ n 1) 2)) 2)
		(expt (fibonacci-rec (/ (- n 1) 2)) 2)))))

这个算法在每次递归调用时都会将问题划分为两个规模为原先一半的子问题,表示该算法运行时间的递归式 (recurence) 为:

T(n) = 2*T(n/2) + c

其中 c 为进行乘法运算和加法运算需要的时间,将其看作常量。而 n/2 代表 (n / 2) 得 到的数的上或下取整得到的整数。由主方法 (master method) 和替换法进行分析都得到这 个算法的时间复杂度应该为 Θ(n),从算法导论上的例子看这个递归式的时间复杂度也确实是 Θ(n),但是从实际的运行情况来看其效率比上面的迭代算法快不少,使用的内存也少许多,与下面的迭代版本的 Θ(lg(n)) 的算法差距不大。(性能比较是在 SBCL 中使用 time 进行的,通过求 F(50000) 进行,这个数值在我的机器上除了 fibonacci-rec-simple 以外 不会进行垃圾回收,因此避免了垃圾回收导致的性能下降。)是不是我的递归式弄错了,请知道的人请告诉我。

避免重复的计算

上面的递归算法中其实还是进行了不少的重复计算,而它的复杂度也因此不能降到 Θ (lg(n))。下面分两种情况考虑上面的递归算法。

如果递归过程中问题划分为求 F(2n) 和 F(2n + 1),F(2n) 会通过 F(n), F(n – 1) 求得, 而 F(2n + 1) 通过 F(n), F(n + 1) 求得。这里的关系如下:

F(2n)     --> F(n - 1), F(n)
F(2n + 1) --> F(n),     F(n + 1)

而如果递归过程中问题划分为求 F(2n – 1) 和 F(2n) 时,有如下关系:

F(2n - 1) --> F(n - 1), F(n)
F(2n)     --> F(n - 1), F(n)

上面的关系式中,F(n) 和 F(n – 1) 都重复出现,但实际上上面的算法都对它们进行了重复计算,减少这些重复计算就可以提高算法的性能。

上面的第二中情况需要计算两个子问题,而第一种情况里面要计算三个子问题,但实际上可以利用 F(n – 1), F(n) 来得到 F(n + 1),因此实际上也只需要计算F(n – 1), F(n) 两个 子问题就可以了。由此我们得到下面的关系式:

F(2n - 1), F(2n) --> F(n - 1), F(n)
F(2n), F(2n + 1) --> F(n - 1), F(n)

注意这里的关系式与前面式子的区别,前面的式子中,每个问题划分为两个子问题,而这里我们将求解两个 Fibonacci 数列中的项看成一个问题,每个问题被划分为一个更小的子问题。利用上面的关系式就可以避免算法执行中的重复计算。由此得到下面的程序:

(defun fibonacci-rec-2 (n)
  (cond ((= n 0) 0)
	(t (multiple-value-bind (x) (fib-two n) x))))
 
;; Returns the n'th and n-1'th fibonacci number.
(defun fib-two (n)
  (cond ((= n 1) (values 1 0))
	((evenp n)
	 (multiple-value-bind (i i-1) (fib-two (/ n 2))
	   (values (* i (+ i (* 2 i-1)))
		   (+ (* i-1 i-1) (* i i)))))
	(t
	 (multiple-value-bind (i i-1) (fib-two (/ (- n 1) 2))
	   (values (+ (* i i) (expt (+ i-1 i) 2))
		   (* i (+ i (* 2 i-1))))))))

fib-two 返回的是 F(n) 和 F(n – 1),利用了 CL 中函数返回多个返回值的特性。这个算法的递归式如下:

T(n) = T(n/2) + c

c 代表加法乘法运算的时间,n/2 的含义与上面的相同。这个递归式的运行时间为 Θ(lg(n))。 实际测试的结果,这个算法的性能是最好的,内存开销也最小(为什么内存开销小不明白,可能跟我使用的 Common Lisp 实现有关),比下面要介绍的复杂度为 Θ(lg(n)) 迭代算法更快。

对迭代的变形提高算法性能

上面的递归算法都是从改变递归关系出发的,通过修改迭代过程也可以得到性能更好的算法。这里的思路是从 SICP 的习题 1.19 中看来的。有兴趣的直接去看原书好了。

考虑之前提到的 T 变换(不是递归式里面的 T),实际上求 F(n) 就是对最初的数值做 n 次 T 变换。

F(n) = T(T(T(...T(1, 0)))) = T^n(1, 0)

改变迭代过程的思路其实使用下面的关系式求乘方是一样的:

a^n = (a^2)^{n/2} = (a^4)^{n/4} = ... = (a^n)^1

具体算法实现的时候,当 n 不是偶数的时老老实实多乘一个 a,是偶数的话就平方。对 Fibonacci 数列,我们希望能够找到一个变换序列 {T_i}(下划线表示下标),使得:

T_1(a, b) = T^2(a, b)
T_2(a, b) = T^4(a, b)
T_3(a, b) = T^8(a, b)
...
T_{k-1}(a, b) = T^{n/2}(a, b)
T_k(a, b) = T^n(a, b)

对任意的 i,下式成立:
T_{i+1}(a, b) = T_i^2(a, b)

SICP 上面用的办法是定义 T_{pq}(a, b) = (bp + aq + ap, bp + aq),然后寻找变换 T_{p’q’},使得:

T_{p'q'}(a, b) = T_{pq}^2(a, b)

通过计算可以得到

p' = p^2 + q^2
q' = q^2 + 2pq

这个式子与之前改变递归定义得到的关系式形式完全相同,不过我没有看出来它们是如何联系起来的。最后给出具体的算法实现,我把书上 Scheme 的实现转换成 CL 实现。

(defun fibonacci-iter (n)
   (fib-iter 1 0 0 1 n))
 
(defun fib-iter (a b p q count)
  (cond ((= count 0) b)
	((evenp count)
	 (fib-iter a b
		   (+ (* p p) (* q q)) ; p
		   (* q (+ q p p))     ; q
		   (/ count 2)))
	(t (fib-iter (+ (* b q) (* a q) (* a p)) ; a
		     (+ (* b p) (* a q))         ; b
		     p q (- count 1)))))

函数 fib-iter 虽然递归调用了自己,但其实是尾递归,实际的思想是迭代。我另外写了一个没有递归调用的版本,但是用了多个 setf 赋值,不够优雅,所以还是给原来的版本。按理说这个算法也是 Θ(lg(n)) 的时间复杂度,但是实际运行中,在求 F(50000) 时仅比 fibonacci-rec 快了没多少,比 fibonacci-rec-2 慢了不少。比后者慢可能是因为这个算法在输入为奇数时只能前进一小步,而 fibonacci-rec-2 不管输入是奇数还是偶数都可以将问题规模缩小一半。

关于递归中的重复计算

为了解决递归中的重复计算问题其实有另外的解决方案,将已经计算过的值存入某个缓存中。函数调用开始计算之前先到缓存中查,如果已经计算过则可以直接利用之前的结果,否则才进行计算。如果实现了这样的缓存,那么即使使用最初的 fibonacci-rec-simple,函数的时间复杂度也会降低到 Θ(lg(n)),而 fibonacci-rec 的时间复杂度也会降到 Θ (lg(n))。这个想法也是看 SICP 时看到的,在 1.2.2 的脚注 34 中提出,练习 3.27 给 出了一个实现。

为什么用 Common Lisp

用 CL 不是为了装 B,我会的语言中用 C/C++, Java 来研究算法显然太麻烦,不支持大整数,性能测试不方便。而 Ruby 的实现会导致一个程序使用不同的语法而导致巨大的性能差异。比如 inject 加 block 的性能比 while 的性能差很多,但是前者在实现累加之类的操作时显然会更优雅。而我对 Scheme 的了解仅限于 SICP 上用到的那些。这样下来就只剩下 Common Lisp 了。

而事实上,用 CL 研究算法真的是非常方便。在 SLIME 和 Emacs 的支持下,我可以使用一个 功能强大的 IDE,编译、调试、代码编辑都非常方便,更重要的是与系统交互式的开发过程。我可以在写完一个函数之后非常方便的测试这个函数,简化测试过程大大的减少了我实现一个算法的时间。另外语言本身的特性也使得算法的实现变得简单,比如让函数返回多个值,CL 本身就有特性支持这个功能。大整数使得我可以使用大的输入来测试性能,time 方便 的让我得到函数运行时间、内存的统计信息。使用其他语言我很难享受到这些好处。

CL 也有不爽的地方:数学式子得转换成前缀表达式,不过这个也可以通过 macro 来解决,Google 到了几个现成的,暂时还没有研究怎么用。

Common Lisp 中使用 ASDF 安装类库和程序

July 9th, 2007 2 comments

虽然要做项目,还要参加比赛,时间不算宽裕,但是就是阻挡不了 Lisp 对我的吸引力。这两天看算法导论,实现算法时打算用 Common Lisp 了,边开发边调试这种环境真是太棒了!

安装 Edi Weitz 的 cl-ppcre (一个可移植的 Perl 兼容正则表达式库) 时稍微看了下 ASDF 的使用,原来还是很简单的。我用的 Common Lisp 实现是 SBCL,ASDF 是自带的,所以安装 ASDF 这一步就省了。下面解释下安装过程。

ASDF 用 asd 文件描述包的依赖性等安装信息,通过这个文件就可以安装某个类库了。为了方便管理,可以把要使用的所有的库的 asd 文件在某个目录下建个符号链接(注意,硬链接不行),ASDF 安装时会到这个目录下去找 asd 文件,如果发现是软链接就会转到目标文件所在的目录开始执行安装动作。使用下面代码可以指定这个存放 asd 文件链接的目录:

(require 'asdf)

(setf asdf:*central-registry*
      '(*default-pathname-defaults*
	#p"/path/to/asd/file/directory/"))

安装库时把解压以后的库代码目录中的 asd 文件在上面的 centeral-registry 目录中建个链接:

$ cd /path/to/asdf/file/directory
$ ln -s ../cl-ppcre/cl-ppcre.asd .

然后就可以通过下面的代码安装这个库,第一次加载时会编译成 fasl 文件,以后加载的时候就不需要编译了。

(asdf:operate 'asdf:load-op 'cl-ppcre)

如果每次启动都会用到这些库的话把这些代码写在 Lisp 实现的启动配置文件中,SBCL 的话就是主目录下的 .sbclrc。就这些,OK 了。

Tags:

关于 Lisp 为什么没有流行的一篇文章 The Bipolar Lisp Programmer

May 21st, 2007 2 comments

很有趣的一篇文章,点这里。作者从分析那些聪明过人,但是最后却没有在学校里取得好成绩的学生为什么失败的原因,总结出他们的一个共有特点:brilliant bipolar mind (BBM)。因为他们聪明过人,所以很多事情在他们看来是 pointless 的(而事实上人类做的很多事情都是 pointless 的),因此不屑去做。

作者由此开始分析造成 Lisp 现状的问题。第一点是 Lisp 对问题的抽象能力强于 C/C++ 类的语言,BBM 们喜欢 Lisp(不知道是 BBM 们先喜欢 Lisp 还是 Lisp 容易让人们变成 BBM)。Lisp 社区通过 Thrown-away design 解决问题就 OK,或者只是为自己的问题考虑,自己理解就行。而使用 C/C++ 解决问题很难,需要许多人共同努力,于是人们写文档,让自己的工作成果可以为别人利用。

另一方面 BBM 们通常都不愿意妥协,作者说这导致了许多结果。一个例子就是 Lisp Machines,不愿意向市场妥协。

关于 BBM 的最后一点是在不景气时的失望、忧郁和遗失自我。Lisp 社区也有人对 Lisp 失去了希望。

作者最后总结到 Lisp 有两个问题,第一个是 Lisp mindset,也就是 BMM 存在的问题。另一个是 Lisp 本身的问题,但是作者最后又说到,

… because Lisp is, like life, what you make of it.

文章提到另一篇经典文章,Lisp: Good News, Bad News, How to Win Big,同样分析了 Lisp 的各个方面。(不过这篇文章发布于 1991 年,到现在也有不少东西发生变化了吧。)

Tags: