翻译者:FreeBluesgit
github版本:https://github.com/FreeBlues/Land-of-lisp-CN程序员
开源中国版本:http://my.oschina.net/freeblues/blog?catalog=516771github
既然咱们已经讨论过 Lisp
的一些哲学而且拥有了一个正常运行的 CLISP
环境,咱们就准备以一个简单游戏的形式来写一些实际的 Lisp 代码。算法
咱们将要编写的第一个游戏是能想到的最简单的游戏。 它是一个经典的猜数游戏。编程
在这个游戏中,你选择一个从1到100的数字,接着计算机要把它猜出来。安全
接下来展现了当你选取数字23时游戏玩起来是什么样子。计算机以50开始猜,而且随着每次不停的猜想,你要输入 (smaller) 或 (bigger) 直到计算机猜中你的数字。编程语言
>(guess-my-number) 50 >(smaller) 25 >(smaller) 12 >(bigger) 18 >(bigger) 21 >(bigger) 23
为了建立这个游戏,咱们须要编写3个函数:guess-my-number,bigger 和 smaller。玩家简单地从 REPL 调用这3个函数就能够了。正如你在前面章节所看到的,当你启动 CLISP (或者其余 Lisp),REPL 将会呈如今你面前,经过它你输入的命令能够被读取(read),而后被求值(evaluated),最后被打印出来(printed)。在这个例子里,咱们要运行命令 guess-my-number,bigger 和 smaller。函数
在 Lisp 中调用函数,你要把这个函数和打算传给这个函数的全部参数一块儿用括号括起来。既然一部分函数不须要任何参数,咱们只要用括号把它们的名字括起来就能够了。布局
让咱们考虑一下这个简单游戏背后的策略。简单思考一下,咱们经过如下步骤来逐步实现:ui
1.肯定玩家数字的上限和下限(大和小)。由于范围在1到100之间,最小的数字应该是1,最大的数字应该是100。 2.在这两个数字之间猜一个数。 3.若是玩家说真实数字更小一些,下降上限(最大数字)。 4.若是玩家说真实数字更大一些,增长下限(最小数字)。
经过上述简单步骤,每次猜想都把可能的数字范围缩小一半,计算机能很快找出玩家的数字。
这种搜索算法被称为二分法(binary search),正如你所知,像这样的二分法一直被用于计算机编程中。你能沿用相同的步骤,例如,高效地找到一个特定的数字,从被给定的数值的排序表中。在这个例子里,你能够简单地追踪表里的最小行和最大行,而且很快找到正确的行,以一种相似的方式。
当玩家调用那些构成咱们游戏的函数时,程序须要追踪下限和上限。为了作到这一点,咱们须要建立两个被称为 *small* 和 *big* 的全局变量。
在 Lisp 中一个被定义为全局的变量被称为一个顶层定义(top-level definition)。咱们可使用函数 defparameter 来建立新的顶层定义:
>(defparameter *small* 1) *small* >(defparameter *big* 1) *big*
函数名 defparameter
会带来一点困惑,由于它实际上没有对参数(parameter
)作任何操做。它所作的就是让你定义一个全局变量(global variable)。
咱们发送给 defparameter 第一个参数是心变量的名字。环绕名字 *small* 和 *big* 先后的星号(*)--被昵称为耳套(earmuffs)--彻底是随意和可选的。Lisp 把星号当作变量名的一部分而且忽略掉它们。Lisper 喜欢以这种方式做为一个约定俗成的惯例为他们的全局变量标上星号,以便它们能够更容易和局部变量区分开来,在本章的后面将会讨论这一点。
注意
尽管耳套在严格技术意义上来讲是可选的,我仍是建议使用它们。我没法保证你的安全,若是你把代码贴到一个 Common Lisp 新闻组而且你的全局变量没有带耳套。
当你使用 defparameter 设置一个全局变量的值时,任何以前存储在这个变量中的值都会被重写覆盖掉:
>(defparameter *foo* 5) FOO >*foo* 5 >(defparameter *foo* 6) FOO >*foo* 6
正如你所见,当咱们从新定义了变量 *foo* 以后,它的值变了。
另外一个能够用来声明全局变量的命令被称为 defvar ,它不会覆盖掉一个全局变量以前的值:
> (defvar *foo* 5) FOO > *foo* 5 > (defvar *foo* 6) FOO > *foo* 5
注意
当你在其余地方阅读关于 Lisp 的知识时,你可能也会看到程序员们在 Common Lisp 中使用术语动态变量(dynamic variable)或特殊变量(special variable)来指一个全局变量。这是由于 Common Lisp 中的全局变量有一些特殊的能力,咱们将会在后面的章节讨论这些。
跟其余语言比起来,Lisp 中命令被调用的方式和代码被格式化的方式有些奇怪。首先,咱们须要用括号把命令(以及命令的参数)括起来,就像 defparameter 函数同样:
>(defparameter *small* 1) *small*
缺乏括号的话,命令不会被调用。
此外,空格和换行被彻底忽略,当 Lisp 读入你的代码时。这意味着你能用任何疯狂的方式来调用这个命令,而结果不会变:
> ( defparameter *small* 1) *SMALL*
由于 Lisp 代码能以这种灵活的方式格式化,Lisper 对于格式化命令有不少约定俗成的惯例,包括何时使用多行和缩进。在本书的代码实例上,咱们将会大体遵循一些常见的缩排惯例。不过,相对于讨论源代码缩排规则咱们更感兴趣的是编写一些游戏,所以本书中咱们将不会在代码布局上花费过多时间。
咱们的猜数游戏经过计算机对玩家请求的响应来开始游戏,而后请求更小或更大的猜想。为了实现这些,咱们须要定义3个全局函数:guess-my-number,bigger 和 smaller。咱们还要定义一个名为 start-over 的函数,用来从新开始游戏以一个不一样的数字。在 Common Lisp 中,用 defun
来定义函数,以下所示:
(defun function_name (参数) ...)
首先,咱们为一个函数指明名字和参数。而后咱们接着写组成函数处理逻辑的代码。
咱们定义的第一个函数是 guess-my-number。这个函数使用变量 *small* 和 *big* 的值来生成一个针对玩家数字的猜想。定义以下所示:
> (defun guess-my-number () (ash (+ *small* *big*) -1)) GUESS-MY-NUMBER
在函数名字 guess-my-number 以后的空括号 () 指明这个函数不须要参数。
尽管在把片断代码输入到 REPL 时不须要担忧缩排和断行,你必须确保把括号的位置放置正确。若是你忘掉一个后括号或者把一个括号放到了错误的位置上,你极可能会获得一个错误。
当咱们任什么时候候像这样在 REPL 里运行一段代码时,输入表达式的结果值将会被打印出来。Common Lisp 中的每个命令都会产生一个返回值。例如 defun
命令简单地返回新建函数的函数名。这就是为何咱们看到在咱们调用 defun
以后在 REPL 中函数名被鹦鹉学舌般返回给咱们。
这个函数作了什么?正如以前讨论过的,这个游戏中计算机最好的猜想将是一个介于两个限制之间的数字。为了完成这一点,咱们选择两个限制的平均值。然而,若是平均值以一个分数结尾的话,咱们想要使用近似(near-average)数,由于咱们猜想的是完整的数字。
咱们在函数 guess-my-number 中实现这些功能经过如下处理:首先把上限值和下限值加在一块儿,,而后使用算数移位函数 ash
,来使上限值、下限值之和减半而且截短结果。代码 (+ *small* *big*)
把这两个变量加起来。由于加法用另外一个函数调用, <1> ,加的结果被接着传递给函数 ash
。
包围函数 ash
和函数 (+)
的括号在 Lisp 中是必需要有的。这些括号告诉 Lisp “我想让你立刻调用这个函数”。
内置的(build-in) Lisp 函数 ash
以二进制的方式看待一个数字,而后把它全部的二进制位(bits)同时移向左边或右边,丢掉在这个过程当中失去的任何位(译者注:)。例如,十进制数字 11
用二进制表达就是 00001011
。咱们能够向左移动这个数字里全部的位,经过 ash
把 1
做为第二个参数:
>(ash 11 1) 22
这样就产生了 22
,二进制是 00010110
。咱们也能够把全部位向右移动(去掉了最后的一位 1
)经过用 -1
做为第二个参数:
>(ash 11 -1) 5
这样会产生5,二进制是 00000101
。
经过在 guess-my-number
中使用函数 ash
,咱们能够连续减半可能数字的搜索空间来快速缩小最终正确数字的范围。正如以前提到的,这种减半处理被称为二分搜索
,一种在计算机编程中颇有用的技术。函数 ash
常常被用于 Lisp 中这些二分搜索
。
让咱们看看当咱们的新函数被调用时将会发生什么:
>(guess-my-number) 50
由于这是第一次猜想,咱们看到调用这个函数的输出告诉咱们一切都按计划进行:程序选择了数字 50
,正好位于 1
和 100
的中间。
在用 Lisp 编程时,你将会写不少函数,它们不会明确打印值到屏幕上。做为替代,它们将会简单地把函数体的计算值返回。例如,咱们说咱们想要一个函数仅仅返回数字 5 ,咱们能够这样写:
> (defun return-five () (+ 2 3))
由于函数体里计算的值被求值为 5,调用 (return-five)
只会返回 5。
这就是 guess-my-number 的设计思路。咱们看到这个被计算后的结果出如今屏幕上(数字 50)不是由于函数使这个数字显示,而是由于这是 REPL 的一个特性。
注意
若是你以前使用过其余编程语言,你可能记得为了让一个值被返回不得不写一些相似 return… 的东西。在 Lisp 中,这是没必要要的。函数体中被计算的最终值会被自动返回
如今要写咱们的 smaller
和 bigger
函数了。像 guess-my-number
同样,这些都是用 defun
定义的全局函数:
> (defun smaller () (setf *big* (1- (guess-my-number))) (guess-my-number)) SMALLER > (defun bigger () (setf *small* (1+ (guess-my-number))) (guess-my-number)) BIGGER
首先,咱们使用 defun
来开始一个新全局函数 smaller
的定义。由于这个函数不带任何参数,因此函数名后面的括号是空的 <1>。
接着,咱们使用 setf
函数来改变咱们全局变量 *big*
的值 <2>。由于咱们知道那个数字必需要比上次猜的值更小一些,最大的它如今是比猜想值要小的那个。代码 (1- (guess-my-number))
这么计算:首先调用函数 guess-my-number
来得到最近的猜想值,而后对这个猜想值使用函数 1-
,会从猜想值里减去 1
。
最后,咱们想要函数 smaller
给咱们显示一个新的猜想值。咱们经过把函数 guess-my-number
放在函数体的最后一行来实现 <3>。这一次,guess-my-number
将会使用更新过的 *big*
值,用这个值来计算下一个猜想值。咱们的函数的最终的值将会自动返回,使得咱们新的猜想值(由 guess-my-number
产生)经过函数 smaller
产生。
函数 bigger
以相同的方式工做,除了它是把 *small*
的值增长以外。终究,若是你调用函数 bigger
,你就是在说你的数字要比上一次猜想值更大,所以最小的它如今要比(就是变量 small
所对应的值)前一次猜想值更大。函数 1+
简单地在由 guess-my-number
返回的猜想值上加 1
<4>。
能够在这里看到当程序猜了 56
时咱们函数的运行状况:
> (bigger) 75 > (smaller) 62 > (smaller) 56
为了完成咱们的游戏,咱们将会增长函数 start-over
来从新设置咱们的全局变量:
(defun start-over () (defparameter *small* 1) (defparameter *big* 100) (guess-my-number))
正如你所见,函数 start-over
重置了变量 *small*
和 *big*
,接着再次调用函数 guess-my-number
来返回一个从新开始的游戏。不论什么时候只要你想启动一个使用不一样数字的崭新游戏时,你均可以调用这个函数来重置游戏。
为了咱们简单的游戏,咱们已经定义了全局变量和全局函数。然而,大多数状况下你可能想把定义限制在一个单独的函数中或者是一块代码内。这些就是被称为局部变量和局部函数。
定义一个局部变量。要使用命令 let
。一个 let
命令有着以下结构:
(let (variable declarations) ...body...)
在 let
命令中的第一部分是一个变量声明的列表。在这里咱们能够声明一个或多个局部变量 <1>。接着,在命令体里(而且仅仅在这个体内),咱们能使用这些变量 <2>。这里是关于 let
命令的一个例子:
> (let ((a 5) (b 6)) (+ab)) 11
在这个例子中,咱们分别为变量 a
<1> 和 b
<2> 声明了值 5
和 6
。这些就是咱们的变量声明。而后,在命令 let
的体内,咱们把它们加在一块儿 <3>,显示出结果值 11
。
在使用一个 let
表达式时,你必须用括号把被声明的变量所有括到一块儿。另外,你必须把每一对变量名字和初始化变量值用另外一对括号括起来。
注意
尽管缩排和断行是彻底随意的,由于在一个 let 表达式里的变量名和它们的值造成了一种简单的表格,提倡的经验是把被声明的变量垂直对齐。这就是为何在上一个例子中 b 被直接置于 a 的下方。
咱们用 flet
命令来定义局部函数。命令 flet
有着以下结构:
(flet ((function_name (arguments) ...function body...)) ...body...)
在 flet
的顶部,咱们声明了一个函数(在起始两行)。这个函数接着在这个主体内将对咱们可用 <3>。一个函数声明包括一个函数名字,函数的参数 <1>,以及函数主体 <2>,在那里咱们将放置函数的代码。
这里是一个例子:
> (flet ((f (n) (+ n 10))) (f5)) 15
在这个例子中,咱们定义了一个独立的函数 f
,它带着一个单独的参数,n
<1>。函数 f
把 10
加到变量 n
上 <2>,被传给它的。=== 接下来咱们使用数字 5
做为参数来调用这个函数,值 15
会被返回 <3>。
跟 let
同样,你能在 flet
的范围内(译者注:也就是在 flet
的顶部)定义一个或多个函数。
一个单独的 flet
命令能被用来一次定义多个本地函数。简单地在命令的第一部分增长多个函数声明就能够了:
> (flet ((f (n) (+ n 10)) (g (n) (- n 3))) (g (f 5))) 12
在这里,咱们声明了两个函数:一个名为 f
<1>,一个名为 g
<2>。在 flet
的主体部分,咱们能够当即使用这两个函数。在这个例子里,主体先使用参数 5
调用 f
获得 15
,接着调用 g
来减去 3
,最终获得的结果是 12
。
为了使得函数名在被定义的函数中也可用(译者注:此处是指同时定义函数能够相互调用),咱们可使用命令 labels
。它的基本结构跟命令 flet
相同。这里是一个例子:
> (labels ((a (n) (+ n 5)) (b (n) (+ (a n) 6))) (b 10)) 21
在这个例子里,局部函数 a
把 5
加到一个数字上 <1>。接着,函数 b
被声明 <2>。函数 b
调用了函数 a
,而后在结果上加 6
<3>。最终,函数 b
使用参数值 10
被调用 <4>。由于 10
加 6
加 5
等于 21
,数字 21
成为整个表达式的最终值。当咱们想要用函数 b
调用函数 a
时 <3>,就须要咱们选择 labels
而不是 flet
。若是咱们用了 flet
,函数 b
是不会"知道"函数 a
的。
命令 labels
容许咱们使用一个局部函数调用另外一个,同时它也容许咱们用一个函数调用它本身。这种作法在 Lisp 代码中很常见,被称为递归(你将会在将来的章节中看到不少关于递归的例子)。
本章中,咱们讨论了用于定义变量和函数的基本 Common Lisp 命令。一路走来,你学到了以下内容:
定义一个全局变量,使用 defparameter
命令。
定义一个全局函数,使用 defun
命令。
分别使用 let
和 flet
命令来定义局部变量和局部函数。
函数 labels
跟 flet
很类似,不过它容许函数自我调用。
调用本身的函数被称为递归函数。