标签 Clisp 下的文章

C程序员驯服Common Lisp – 入门

毫无疑问,Common Lisp是一门庞大且复杂的语言,学习曲线并不平坦。对于一个从未接触过函数式语言、交互式语言以及动态类型语言的C程序员来说,学习Common Lisp显然是一个很大的挑战。

也许有人会问:"C语言已经无所不能了,为何还要学习Common Lisp?"在这里我不想说太多冠冕堂皇的话,至少对我而言,理由有三:
一是好奇,在C语言的世界里待得久了,总想探出头来吸几口新鲜空气,这次我选择了Common Lisp;
二是为了变成一名更好的程序员。为何学习Common Lisp就能成为一名更好的程序员呢?这不是我的观点,而是诸多牛人或大师们(包括Paul GrahamPeter Norvig以及另外一个Peter:Peter Seibel等)的观点。不过不管你们信不信,反正我是信了。这个观点的关键思想就是一门语言可以影响一个程序员的思维方式。我相信Common Lisp可以给我带来一种不同于以往的新的编程思维方式,这样至少比只有一种思维方式要好,不是吗;
最后,Lisp是一门可编程的编程语言,可以很容易扩展自身并且创造一门新的语言。我无法不动心于如此一门强大的语言。

学习总是需要一些付出的。Jolt大奖得主《Practical Common Lisp》的作者Peter Seibel花了一年的时间放下一切潜心学习Common Lisp并终有所成。我们还有工作,有生活压力,无法像Seibel那样潇洒,但我们依旧可以去学习Common Lisp,循序渐进地学,一步一步来"驯服"Common Lisp这个"猛兽"。"猛兽"被驯服后,才能为你所用,发挥出异常的威力,不是吗?我们需要的仅是恒心和足够的耐心罢了。

"驯服"意味着"学会",何为学会一门语言?只是知晓语法,看懂代码还远远不够,那些仅仅叫知道或了解或"纸上谈兵",还谈不上真正地"学会"。古人云:"学以致用",只有在实际中可以灵活自如的使用了,才叫真正的"学会"了。

现在只是开始!这里我会按照C程序员学习C语言的逻辑展开,为了更加贴近C程序员的思维模式,我选择了这种相对平滑的学习方式。也许最初的几篇会让你觉得Common Lisp很像一门命令式语言^_^!

言归正传!学习一门编程语言之前,最好先弄清楚该语言在当前众多语言中的位置,了解一下它的前世今生,这有助于你对这门语言的认知。不过关于Common Lisp的详细历史这里就不赘述了,在进行下面内容之前,请先阅读一下维基百科,或是读读几本经典Common Lisp书籍(如《ANSI Common Lisp》、《On Lisp》以及《Practical Common Lisp》等)中对Common Lisp历史的介绍。

Common Lisp是Lisp语言大家族中的一分子,和Scheme等一样,它也是一门Lisp方言(Dialect)。与C语言相比,Lisp更加古老,是史上第二古老的编程语言,仅次于Fortran。但Common Lisp比C年轻,它是在上世纪80年代诞生的。与C语言普遍采用的"编辑->编译->调试/执行"的工作方式不同,Common Lisp更多采用的是类似于Python、Ruby那样的交互式的解释器工作模式。你在Common Lisp交互环境中就可以完成上述C语言的所有步骤。这种方式目前看来更易于语言的学习(虽然C语言目前也有解释器的实现,如Ch,但C程序员似乎更喜欢传统方式)。

目前市面上Common Lisp的实现有很多种,既有商业收费的,也有开源免费的。商业软件这里就不提了,常用的免费开源的主流Common Lisp解释器包括CLISPSBCL(Steel Bank Common Lisp)和Clozure CL。我个人更喜欢使用CLISP,所以后续有关解释器方面的内容更多以CLISP为主。

CLISP支持诸多平台,你可以很容易得到安装包并顺利的完成安装,关于这方面内容这里就不赘述了。打开一个终端(Windows下打开一个命令行窗口),敲入"clisp",回车,你就进入到CLISP提供的Common Lisp顶层环境(Top-Level)当中了(若要进入SBCL,敲入sbcl;若要进入Clozure CL,敲入ccl,以上的前提是这些包的可执行程序路径已经加入到你的PATH环境变量中了),就像这样:

$ clisp
… …
Welcome to GNU CLISP 2.44.1 (2008-02-23) <http://clisp.cons.org/>

Copyright (c) Bruno Haible, Michael Stoll 1992, 1993
Copyright (c) Bruno Haible, Marcus Daniels 1994-1997
Copyright (c) Bruno Haible, Pierpaolo Bernardi, Sam Steingold 1998
Copyright (c) Bruno Haible, Sam Steingold 1999-2000
Copyright (c) Sam Steingold, Bruno Haible 2001-2008

Type :h and hit Enter for context help.

[1]> _

对于所谓的"顶层环境",熟悉Python和Ruby等解释型语言的朋友并不陌生。它就是一个已经加载了标准Common Lisp包的REPL环境。其中REPL是Read-Eval-Print-Loop的缩写。说白了,这就是一个Common Lisp代码的执行环境,你在里面可以输入Common Lisp代码,这些代码可以被直接执行,执行结果也会立刻展现在你的眼前,或如果遇到错误/异常时,你还可以在里面直接进行代码调试。当然了"顶层"还有一个范围(Scope)的概念在里面,用于区分不同变量和函数的作用域。

我们在CLISP中输入一些字符串、字符以及数字以及简单表达式:

[1]> "hello lisp"
"hello lisp"
[2]> #\c
#\c
[3]> 1
1
[4]> (+ 1 2)
3

CLISP对于我们的输入给予了回应:对于字符串、字符(注意Common Lisp的字符表示法很特别,以#\作为前缀,#\c即C语言中的'c')以及数字,CLISP进行了回显(实际上是对输入求值后的结果),对于"(+ 1 2)"这个计算1和2之和的表达式,CLISP给出了求值后的结果。

我们继续输入一个a:

[5]> a

*** – EVAL: variable A has no value
The following restarts are available:
USE-VALUE      :R1      You may input a value to be used instead of A.
STORE-VALUE    :R2      You may input a new value for A.
ABORT          :R3      Abort main loop
Break 1 [6]>

与前面不同的是,这次CLISP给出了错误提示,求值器(evaluator)无法找到a绑定的值,CLISP进入异常处理模式,或称作调试模式。CLISP给出了三种选择:我们选择输入:R3,可以回到top-level主循环;选择输入:R2,则可以为a赋值。

Break 1 [6]> :R2
New A: 5
5
[7]> a
5

SBCL和Clozure CL与CLISP类似,都会有类似的调试模式,退出调试模式的方法参见各自的提示说明即可。

如果要退出CLISP解释器,我们可以输入"(quit)",注意quit两边的括号也是命令的一部分;在SBCL中,我们可以输入(SB-EXT:QUIT)退出;Clozure CL的退出方法与CLISP相同。

Common Lisp源代码是由一组S-expressions(symbolic expression)构成的。什么是S-expression呢?这个在Common Lisp书籍中很难找到答案,因为S-expression是一种组织数据的结构,并不是Lisp独有的,只是Lisp恰好也采用了这种结构来组织存储Lisp的代码和数据罢了。在维基百科上,S-expression有一个递归的定义:"S-expression要么是一个被成为原子(atoms)的单一的数据对象(data object),要么是一个S-expressions列表(list)。数字、数组、字符串以及符号都是原子",比如:

[1]> 13
13
[2]> #(1 2 3)
#(1 2 3)
[3]> "hello"
"hello"
[4]> #'length
#

数字'13'、数组'#(1 2 3)'、字符串"hello"以及符号'length'都是原子。

Lisp将代码和数据都存储于S-expressions当中,这是Lisp与其他主流语言的最大区别之一。我们在编写Common Lisp源码时,需要遵循正确的S-expression格式。前面说过Common Lisp解释器就是一个READ-EVAL-PRINT-LOOP环境,这个环境主要由一个Reader和一个Evaluator构成。Reader负责读取源文件中的文本或者我们在提示符后面输入的文本,检查文本格式是否符合S-Expression要求,直到所有文本都符合格式要求,这样解释器就得到了正确的S-expression:

[1]> (+ 1 2))
3
[2]>
*** – READ from … >: an object
      cannot start with #\)

通过上面例子可以看出,Reader识别出了不符合S-expression格式的源码文本。

Reader将文本转换为S-expressions后,Evaluator就开始对S-expression进行校验,校验其是否符合Lisp Code的规范形式(Lisp Form)。

下面的例子说明了Evaluator的作用:

[1]> (foo 1 2)
*** – EVAL: undefined function FOO

毫无疑问,(foo 1 2)是一个有效的S-expression,其通过Reader这关是没有问题的。但是当Evaluator对S-expression"(foo 1 2)"进行验证求值时,却发现无法找到函数foo的定义,这行源码不合法。

简单总结Reader和Evaluator的工作流程就是:"源码文本"通过Reader转换为有效的"S-expressions",后者则由Evaluator转换成有效"Lisp Form"并求值得出结果。

Common Lisp初学者常常被那满眼的括号所吓住,不过事实上括号并没有那么"可怕"。括号其实主要是给Common Lisp解释器(Reader和Evaluator)用的,而不是给程序员看的。现今的代码编辑器都很智能,基本上可以消除括号在编程过程中给你带来的影响(要说一点影响没有也不太可能)。

Common Lisp支持多种注释形式。在C语言中我们用'//'进行单行注释(C99标准引入),而Common Lisp的单行注释符号为';'。C语言采用'/*…*/'进行多行注释,Common Lisp使用的是'#|…|#'。Common Lisp还提供了一种大多语言都不具备的注释方式,那就是将注释直接写到紧邻函数定义的参数列表后面的位置上,这样通过Common Lisp提供的工具,我们可以轻松地提取出该函数的注释,并生成代码文档,比如:

[1]> (defun foo (x) "test comments" (+ x 1))
FOO
[2]> (documentation #'foo t)
"test comments"

由于Common Lisp括号众多,一个风格良好的Lisp程序需要通过良好风格的代码缩进来保证,这方面我推荐AI领域大师Peter Norvig若干年前编写的一篇有关优秀Lisp编程风格的文章《Tutorial on Good Lisp Programming Style》。

很多C程序员可能还是习惯于将代码写到文件中。Common Lisp解释器提供了将你的源文件加载到顶层环境并直接使用其中的定义的方法:
;; foo.lisp
(defun foo (x) "test foo"
   (+ x 1))

[1]> (load "foo.lisp")
;; Loading file foo.lisp …
;; Loaded file foo.lisp
T
[2]> (foo 5)
6

利用load函数我们可将你的源文件加载到顶层环境中,并在顶层环境里使用该源文件中定义的函数。

编程语言初学者总喜欢在终端控制台上看到自己编写的程序的输出结果,那样会产生一种奇妙的成就感,程序员们多陶醉于其中。C程序员最常用的就是printf函数了,Common Lisp中也有与printf等价的函数,它就是format。这里不是专门讲解format函数的,下面仅仅列举一些常见的例子,这些例子应该可以满足你在学习语言初期的需求了:

* 输出整型数
(format t "~d" 1000000) ==> 1000000
(format t "~x" 1000000) ==> f4240
(format t "~o" 1000000) ==> 3641100
(format t "~b" 1000000) ==> 11110100001001000000

上面依次是按十进制、16进制、八进制和二进制输出。

* 输出浮点数
(format t "~f" 3.1415) ==> 3.1415

* 输出字符串
(format t "~a" "hello lisp") ==> hello lisp

* 输出字符
(format t "~c" #\c) ==> c

* 输出换行符
以下借用《ANSI Common Lisp》书中的一个例子:
(format nil "Dear ~a, ~% Our records indicate…" "Mr. Malatesta")
==> "Dear Mr. Malatesta,
 Our records indicate…"

format函数的第一个参数表示是否输出到"标准输出(*STANDARD-OUTPUT*",如果传入t,则表示输出到标准输出设备上。第二个参数与C中的printf函数的第一个参数类似,是一个格式串,不同的是格式串中的指示符(directive)由printf中的'%'变成了'~'。

为了让大家更加直观地了解Common Lisp源代码到底是什么样子的,下面将给出一个Common Lisp的例子程序,这个程序用来计算参数字符串中大写字母的总个数:

我们先给出一个命令式风格的实现版本:
;; upper-char-counter.lisp
(defun upper-char-counter (str)
  (let ((len (length str)) (result 0))
      (do ((i 0 (+ i 1)))
          ((>= i len) result)
        (if (upper-case-p (char str i)) (setf result (1+ result))))))

即使你不懂Common Lisp语法,你也能大致猜测处理这段代码的逻辑,基本上与下面C代码是等价的:
int upper_char_counter(const char *str) {
    int result = 0;
    int len = strlen(str);

    int i = 0;
    while (i < len) {
        if (str[i] >= ‘A’ && str[i] <= 'Z') {
            result++;
        }
        i++;
    }

    return result;
}
 
下面是一个函数式风格的实现版本:

;; upper-char-counter.lisp
(defun upper-char-counter (str)
   (count-if #'upper-case-p str))

[1]> (load "upper-char-counter.lisp")
;; Loading file upper-char-counter.lisp …
;; Loaded file upper-char-counter.lisp
T
[2]> (upper-char-counter "a5B6CD!")
3

这个版本的代码显然更加简洁,但理解起来有些难度。函数count-if接受一个函数和一个字符串作为参数,count-if将函数upper-case-p应用于str中的各个字符上,并将返回true(t)的结果个数累加得到最终返回值。

走到这里,我想大家应该对Common Lisp有了一个感性的认识了,至少可以编写一些命令式风格的简单代码或复制一些现存的代码放到顶层环境中执行了。如果真的是这样,那我的目的就达到了^_^。

Common Lisp初学点滴

Common Lisp是一门Interactive语言,比较容易上手。无论你是用CLISPSBCL还是Clozure CL,你都可以很快地写出一个"Hello, World"程序出来。不过千万不要因此低估了Common Lisp,前人的经验表明:Common Lisp是门庞大且复杂的语言,其学习曲线可并不低。要想真正掌握它,需要你有持续的热情、足够的耐心和不断的练习。我接触Common Lisp时间也不长,是个地地道道的初级选手。这段时间看了些书,做了一些练习,这里把我初学Common Lisp过程中的点点滴滴记录下来,以备忘。

俗话说:工欲善其事,必先利其器。Common Lisp开发者们也有着自己一套高效的开发工具。目前无论是在Windows还是在Linux或是其他平台上,最受Lisper们推崇的工具组合是Emacs+ Slime(The Superior Lisp Interaction Mode for Emacs)。鼎鼎大名的Emacs这里就不说了,Slime对于很多非Lisp开发者来说是一个陌生的名字,我们可以把它看成是一种专门为Lisper们提供的一个嵌入到Emacs中的IDE,通过它我们可以在Emacs编辑器中直接进行Lisp代码的求值,编译,宏扩展,符号定义的查找,名字的自动补全以及在线文档查询等操作。我平时开发更多使用的是另外一种编辑神器-VIM,幸运的是已经有人将Slime移植到了Vim下,Slime摇身一变,变成了Slimv(The Superior Lisp Interaction Mode for Vim)。由于接触时间较短,我目前尚不确定在功能上Slimv是否完全等同于Slime。不过就目前来看,Slimv的确让Vim下Common Lisp代码的编写变的高效了许多。

Slimv的安装极其简单:将Slimv包下载到你的$HOME/.vim下(这里以Linux下的安装为例),直接解压即可。Slimv首先为Vim提供了一种名为Paredit Mode(.vim/doc/paredit.txt )的编辑模式,这种模式专门针对Lisp代码源文件,诸如以.lisp为后缀名的文件。该编辑模式保证内容中所有括号、方括号以及双引号均平衡出现,即成对匹配。当你敲入"(",该模式会自动补充对应的")";删除半个括号时,另半个括号也被自动删除。初次使用Paredit mode很不习惯,特别是不知如何在括号的外层再包裹一层括号,也就是将(list 1 2)变为((list 1 2))。每次在(list 1 2)开始处输入"(",都会得到"()(list 1 2)"。后来才在Stackoverflow上觅到答案:原来先输入"\"再输入"("时,Slimv不会自动补充")",通过这种方式可以在括号的外围再加上一层括号了,在Lisp实际编程过程当中,嵌套括号的情况还是很多的。

打开一个名为xx.lisp的源文件,Slimv就会自动发挥作用。在Vim的命令模式下,敲入",c",Slimv会自动启动Swank Server,这个Server运行着一个Common Lisp的REPL,接收并处理嵌入在Vim中的Slimv client端发出的求值、编译、调试等请求,保存你在Vim中与REPL的session内容。Slimv同时会在Vim里创建一个REPL窗口,不过这仅是用来等待你的输入,真正的求值等操作是在Swank Server完成的。

Slimv会自动Detect你已安装的Common Lisp实现,在我的已经安装过Clisp和SBCL的系统中,Slimv优先选择了SBCL。 关于Slimv,这里不再多说什么了,因为其作者已经编写了一份很详尽的Tutorial在这里,有兴趣的朋友可以参考之。

我在读的Common Lisp书籍主要有两本:一本是"黑客与画家"的作者Paul Graham编写的"ANSI Common Lisp",另外一本则是Peter Seibel的"Practical Common Lisp"(据说该书的中文译本已由binghe完成)。这一周多来,我快速地浏览了Peter Seibel的"Practical Common Lisp",除了惊奇于一些之前未曾接触过的特殊语法结构(如Closure)之外,也感叹于Common Lisp的复杂,数不尽的function, macro和special operator让我有些迷失和混淆。另外Peter Seibel自称书中有关macro的例子都很初级,但就是这样初级的macro也是甚难以理解的。关于macro的深入领会,我看只能指望Paul Graham的大作:"ANSI Common Lisp"和"on lisp"了。

另外一本名为"Common Lisp Quick Reference"的小书也值得一看,不过更适合Common Lisp老手查阅手册时使用。

浏览完"Practical Common Lisp“后,继续精读"ANSI Common Lisp",并且对其中的习题也不放过。这些练习估计很初级,不过对于我这个初级选手来说正合适。刚刚看完第二章(Welcome to Lisp),这里将我的习题答案放到这里,供大家批评指正:

练习1.
(a) 14
(b) (1 5)
(c) 7
(d) (NIL 3)

练习2.
[1]> (cons 'a '(b c))
(A B C)
[2]> (cons 'a (cons 'b (cons 'c nil)))
(A B C)
[3]> (cons 'a (list 'b 'c))
(A B C)

练习3.
[1]> (defun my-fourth (x)
          (car (cdr (cdr (cdr x)))))
MY-FOURTH
[2]> (my-fourth '(1 2 3 4 5))
4

练习4.   
[1]> (defun my-max (x y)
         (if (> x y) x y))
MY-MAX
[2]> (my-max 5 6)
6
[3]> (my-max 7 6)
7

以上方案只适用于整数等适用>进行比较的类型,下面是一个更加通用的版本:

[1]> (defun my-max1 (x y comp_func)
         (if (funcall comp_func x y) x y))
MY-MAX1
[2]> (defparameter *cf* (lambda (x y) (if (> x y) t nil)))
*CF*
[3]> (my-max1 5 6 *cf*)
6
[4]> (my-max1 7 6 *cf*)
7
[5]> (defparameter *ccf* (lambda (x y) (if (char> x y) t nil)))
*CCF*
[6]>  (my-max1 #\c #\b *ccf*)
#\c
[7]> (my-max1 #\c #\d *ccf*)
#\d

练习5.
(a) enigma函数的功能是找出list中是否有值为nil的元素,如果有,返回T;否则返回nil
(b) mystery函数的功能是返回x在y列表中的位置(下标)

练习6.
(a) x = car
(car (car (cdr '( a (b c) d ) ) ) )

(b) x = or
(or 13 (/ 1 0))
注:短路求值,后一项在13为t的情况下不被求值,避免了divide by 0错误

(c) x = apply

注意funcall与apply的区别
(funcall function arg1 arg2 …)
==  (apply function arg1 arg2 … nil)
==  (apply function (list arg1 arg2 …))

练习7.
(defun have-list-param-p (x)
  (let ((result nil))
    (dolist (obj x)
      (if (listp obj)
        (setf result t)))
    result))

[1]> (load "list_param.lisp")
;; Loading file list_param.lisp …
;; Loaded file list_param.lisp
T
[38]> (have-list-param-p '(1 2 3))
NIL
[39]> (have-list-param-p '(1 (2 3) 4))
T

练习8.
(a)
iterative solution:
(defun print_dots (number-of-dots)
  (do ((i 1 (+ i 1)))
    ((> i number-of-dots))
    (format t ".")))

recursive solution:
(defun print_dots (number-of-dots)
  (let  ((i number-of-dots))
     (if (> i 1)
        (print_dots (- number-of-dots 1)))
     (format t ".")))

练习9.
(a) 问题所在:remove返回一个新的lst,原来的lst如果包含nil,则+会提示nil is not a number
修改后:
(defun summit (lst)
  (setf lst (remove nil lst)) 
  (apply #'+ lst))

(b) 问题所在:导致无穷递归,提示Program stack overflow. RESET
修改后:
(defun summit (lst)
  (if lst (+ (or (car lst) 0) (summit (cdr lst))) 0))
     
Common Lisp与Haskell不同,Common Lisp并非纯函数式编程语言,其中包含了诸多命令式(imperative)的元素,这样对于习惯了命令式编程的初学者来说,在学习过程中就不会感觉到过于剧烈的思维跳跃了。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats