标签 Common-Lisp 下的文章

C程序员驯服Common Lisp – 表达式

Common Lisp程序由一组表达式构成。在"入门"一文中我提到过:Common Lisp使用S-expressions作为表达式(Expressions)的基本语法格式。S-expressions由原子(atoms)和S-expressions列表组成,或者说原子和列表(List)是组成S-expression的基本元素。复杂的源程序都是由简单的表达式组成的,我们在学习编写实用的Common Lisp程序之前,首先要清楚简单表达式的结构和求值方法。

每个Lisp表达式都可以提交给Common Lisp解释器进行求值,并得到一个求值结果。这里我们从简单的原子说起。

一、原子
原子虽然十分简单,但它也是一种表达式。对于原子而言,其求值结果就是其自身的值。下面我们来看看一些常见的原子以及其求值结果:

(1) 数字(Number)
数字是一种原子,其求值结果即为其自身数值。

* 整型数字
[1]> 13
13
[2]> -4
-4
[3]> 0
0
[4]> #xa    ;; 16进制数
10
[5]> #o11   ;; 8进制数
9
[6]> #b011  ;; 二进制数
3
[7]> #24r1n ;; 24进制数
47

最后的#24r1n是一种通用N进制数表示形式,N取值范围为2到36,其表示形式为#Nr…。

* 浮点数
[1]> 3.1415
13
[2]> 365e0
365.0
[3]> 365e-3
0.365
[4]> 365f-3
0.365
[5]> 365d-3
0.365d0
[6]> 0.365e20
3.65E19

标志f表示单精度浮点数,标志d表示双精度浮点数,标志e表示默认采用单精度,与f相同。

* 分数
[1]> 5/6
5/6

* 复数
[1]> #C(1.2 3)
#C(1.2 3)

C(1.2 3)对应的复数为1.2+3i。

(2) 字符(character)
单独的字符也是原子,其求值结果也是其自身值。

下面是一些可见字符:
[1]> #\a
#\a
[2]> #\A
#\A
[3]> #\%
#\%
[4]> #\&
#\&

一些常见的控制字符的形式如下:
[1]> #\newline
#\Newline
[2]> #\tab
#\Tab
[3]> #\backspace
#\Backspace
[4]> #\space
#\Space
[5]> #\escape
#\Escape

(3) 字符串(string)
C语言中的字符串不同,Common lisp中字符串结尾并不包含''。但字符串也是原子,其求值结果依旧是其本身。

[1]> "Hello, Common Lisp!"
"Hello, Common Lisp!"

我们可以字符转义将一些特殊字符放入字符串,比如我们可以在字符串中包含双引号:
[2]> (format t "~A ~%" "He said \"I am going to see Harry Potter!\" and then he left.")
He said "I am going to see Harry Potter!" and then he left.
NIL

不过我们无法通过转义方法将tab字符、回车符、换行符放入字符串,只能通过键盘手工输入:
[3]> "Look, here are      tabs      and some

returns!

Understand?"
"Look, here are      tabs      and some

returns!

Understand?"

(4) 布尔类型(bool)
布尔值也是原子,但只有两个可选值:t和nil。t代表true,nil代表false,由于比较简单,这里就不细说了。

(5) 符号(symbols)
C语言不同的是,Common Lisp中有一种语法元素称为"符号"。符号有些类似于C语言中的标识符,用来表示Lisp程序中使用的名字,诸如函数和变量的名字,像format,hello-foo, *counter*等。Lisp符号的包容性更强,像+,-等在C中为操作符关键字的字符都可以作为符号。

(a b c)       ; 一个包含三个符号的list
(a 2 "bar")   ; 这个list包含一个符号,一个数字和一个字符串
(+ (* 2 3) 4) ; 这个list包含一个符号,一个列表以及一个数字

符号的求值比较特殊,如果该符号没有绑定到任何值,解释器会提示错误。如果绑定了值,则显示绑定的值:
[1]> 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 [2]> :r3
[3]> (setf a 5)
5
[4]> a
5

二、列表
在实用程序中,我们很少将原子单独作为表达式,我们更多使用的是List,即列表。之前说过Common Lisp的核心就是List,此List不同于我们以往数据结构中学习的那个List,在Common Lisp中,List是程序和数据的载体,别忘了Lisp是"LISt Processing"的缩写,直译过来Lisp就是List处理语言,这也凸显了List在Lisp语言中的核心地位。

绝大多数情况下,Lisp程序字面上就是一组列表集合。掌握对List进行求值的方法就显得尤为重要了。

我们先从一个简单到不能再简单的List入手:
[1]> (+ 1 2)
3

这个List由三个原子组成,一个符号以及两个数字。Common Lisp解释器会首先检查第一个元素是否是一个符号并且是否是一个绑定了有效函数的符号。如果不符合条件则报错。如果第一个元素是符号且绑定合法函数,如+,那么解释器会将后续的元素作为该函数的参数,并自左向右对参数逐个进行求值。

[1]> (length "hello lisp")
10
[2]> ("foo" 1 2)

*** – EVAL: "foo" is not a function name; try using a symbol instead
The following restarts are available:
USE-VALUE      :R1      You may input a value to be used instead.
ABORT          :R2      Abort main loop

在(+ 1 2)这个例子中,+是一个符合条件的符号,解释器接下来对1和2这两个原子进行求值,前面提到整数是原子,其求值结果即为自身值,所以解释器将1和2传给+,得到最终结果3。

Common Lisp的解释器对参数的求值是自左向右递归进行的,下面是一个稍复杂的表达式,其详细的求值过程如下:

(+ (- 7 (/ 4 2)) (* 3 4))
-> (+ (- 7 2) (* 3 4))
-> (+ 5 (* 3 4))
-> (+ 5 12)
-> 17

解释器从左向右依次对参数进行求值,解释器遇到函数+的第一个参数(- 7 (/ 4 2)),这显然是也是一个减法表达式,解释器递归地对该表达式进行求值;(- 7 (/ 4 2))表达式的第一个参数7为原子类型,其求值结果为自身值7;第二个参数又是一个除法表达式,解释器再一次进行递归求值,进入(/ 4 2),这是个简单表达式,其求值结果为2;求值程序回到表达式(- 7 2),得到求值结果5,至此最外层表达式的第一个参数求值完毕,结果为5;解释器继续对最外层表达式的第二个参数(* 3 4)进行求值,这是个乘法表达式,对该表达式求值结果为12,这样我们的顶层表达式就变成了(+ 5 12),则最终求值结果就为17。这个过程有点类似于树遍历算法中的深度优先遍历算法。

无论是多么复杂的表达式,Common Lisp解释器的求值方法都是如此的。当然解释器不一定会将函数的所有参数都进行求值,比如:(if t 5 (+ 6 7)这个表达式在if条件为t时,只会求值5这个参数,(+ 6 7)这个表达式不会被求值。

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有了一个感性的认识了,至少可以编写一些命令式风格的简单代码或复制一些现存的代码放到顶层环境中执行了。如果真的是这样,那我的目的就达到了^_^。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系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