10 June 2013

早闻lisp的大名却一直没有机会一窥其真容。曾经读过一些相关的书籍,包括Paul Graham的《黑客与画家》(Hackers and Painters: Big Ideas from the Computer Age)、《编程人生》(Coders at Work)以及一些人工智能相关书籍,lisp对于我就是一片神奇的大陆,有待探索。

早早就买了这本《计算机程序构造与解释》(Structure and Interpretation of Computer Programs),这本书又被黑客称作“魔法书”,其内容是关于lisp 的一门方言scheme的编程方法,与其说是介绍scheme,不如说是传授一种编程思想。其早先是MIT的一门编程课程的教材(可惜MIT现在用python取代了scheme),目前仍然是百所大学的课程教材。一直以来由于各种原因没有仔细阅读这部著作,其实早有耳闻这本书的难度很大,不久前,终于自己下定决心要啃下这块硬骨头,目前已经看到第二章,习题做了90%以上,现在就让我谈谈初学scheme的感受吧。


scheme真的是一门优美的语言

随着学习的深入,我发现自己已经深深喜欢上这门语言了。你会说代码中全是括号,优美体现在哪里?我想“美”对每个人来说都不同,但是scheme的美体现在了它的简洁上,它的一些特性使它能够将各种实现极简化。以下我要说说在第一章中哪些特性是我所喜爱和推崇的。

递归和迭代

举个书中的例子,阶乘。相信大家都知道阶乘的计算过程:n! = n * (n - 1) * (n - 2) * … * 3 * 2 * 1 。 假设我们用C来编写阶乘过程,应该怎么写呢,下面是递归过程:

#include <stdio.h>

int main(void)
{
	int n;
	scanf("%d", &n);

	printf("%d\n", factorial(n));
	return 0;
}

int factorial(int n)
{
	if (n != 1)
		return n * factorial(n - 1);
	else
		return 1;
}

我们来看看scheme怎么实现上面的递归过程:

(define (factorial n)
  (if (= n 0)
      1
      (* n (factorial (- n 1)))))

我们可以看到,使用scheme后,代码有多么简洁。而scheme的迭代则更加神奇。 用C来实现迭代方法可以使用循环,那我们先用C来实现迭代阶乘计算过程:

#include <stdio.h>

int main(void)
{
	int n;
	int total;
	scanf("%d", &n);
	
	for (total = 1; n > 0; n--)
		total *= n;
	printf("%d\n", total);
	return 0;
}

我们再用scheme实现上面过程:

(define (factorial n)
  (fact-iter 1 1 n))

(define (fact-iter product counter max-count)
  (if (> counter max-count)
      product
      (fact-iter (* counter product)
                 (+ counter 1)
                 max-count)))

是不是很奇怪,scheme的迭代和递归怎么这么像,没错,这正是被叫做尾递归的方法,scheme正是用尾递归来实现循环。

函数式编程(Functional Programming)

面向对象编程大家耳熟能详,但函数式编程对我却是一片空白。维基百科给出了函数式编程晦涩的解释:函数式编程或者函数程序设计,又称泛函编程,是一种编程范型,它将电脑运算视为数学上的函数计算,并且避免状态以及可变数据。函数编程语言最重要的基础是 λ 演算(lambda calculus)。而且λ演算的函数可以接受函数当作输入(引数)和输出(传出值)。 由于自己也是初步学习scheme,这里给出一些不成熟的看法。

那么,还是举例能够更加直观,如果做一个整数累加,大家可能会觉得写一个递归或迭代就可以实现,那如果实现一个整数平方的累加呢?那也不难,每个整数做个平方处理就可以,使用函数或者不使用函数都可以实现。那如果实现一个整数立方的累加呢,每次改变都需要对代码进行更改。也许你又要提出使用函数指针的方法,这当然是可以的。那么我们要实现奇数的累加岂不是子函数也要更改?再复杂一点的过程,需要更改的部分将会更多,不仅复杂,更加容易出错。我们来看看scheme是如何实现这个过程的:

(define (sum term a next b)
  (if (> a b)
      0
      (+ (term a)
         (sum term (next a) next b))))

上面的scheme代码看上去很奇怪,term,next都不知道是什么。其实这段代码就是一个“模板”,term,next是你需要向其中添加的过程(函数),我们使用这个模板再举几个例子,我们来实现整数平方累加:

(define (sum term a next b)
  (if (> a b)
      0
      (+ (term a)
         (sum term (next a) next b))))

(define (sum-square 1 4)
  (sum square 1 1+ 4))

(define (square x)
  (* x x))

这里1+是自加1,我们看到模板中的term被square替代,term被1+替代。如果我们要实现整数立方累加,只需将term替换为立方过程。如果要实现每隔一个整数进行累加,则把next替换为“自加2”即可。再复杂的过程都可以如此实现,也可以修改模板使其适用范围更广。

lambda算子

上面讲述的函数式编程是不是很好很强大,那么lambda算子更是为其锦上添花。举个例子:

(define (plus4 x) (+ x 4))

(lambda (x) (+ x 4))

这两段代码的实现过程是相同的,但是细节处理上却有很大的不同。首先使用lambda算子则不需要具体的过程命名,即不需要命名plus4。lambda后面的括号是参数,当然可以是(x y),甚至是其他的过程。那么我们使用lambda算子重现“间隔四个整数立方累加”的过程:

(define (sum term a next b)
  (if (> a b)
      0
      (+ (term a)
         (sum term (next a) next b))))

(define (sum-cube-odd 1 10)
  (sum (lambda (x) (* x x x))
       1
       (lambda (x) (+ x 4))
       10))

尽管多了很多“lambda”,但是代码明显比刚才的例子更加简洁,消除了square,plus4这些过程的定义过程。 scheme还有一种强大的表达式“let表达式”,在此不过多介绍。

经过上面介绍,scheme的优美与强大也许已经初现端倪,而随着后续的学习深入,我想scheme还会给我带来更多的惊喜。 如果你对SICP(计算机程序设计与解释)感兴趣,网上有其免费英文版本。 这里是我的习题解答



blog comments powered by Disqus