Common Lisp/初学者教程/经验丰富的教程
本章介绍了一些关于 Lisp 程序结构的理论基础。
Lisp 操作的是表达式。每个 Lisp 表达式要么是一个原子,要么是一个表达式的列表。原子是数字、字符串、符号和其他一些结构。Lisp 符号其实很有趣 - 我将在另一节中讨论它们。
当 Lisp 被强制执行表达式时,它会查看它是一个原子还是一个列表。如果它是一个原子,则返回它的值(数字、字符串和其他数据返回自身,符号返回它们的值)。如果表达式是一个列表,Lisp 会查看列表的第一个元素,称为它的car(一个过时的术语,代表Contents of the Address part of Register)。列表的 car 应该是一个符号或一个 lambda 表达式(lambda 表达式将在后面讨论)。如果它是一个符号,Lisp 会获取它的函数(与该符号关联的函数 - 而不是它的值),并使用从列表的其余部分获取的参数执行该函数(如果它包含表达式,它们也会被执行)。
例如:(+ 1 2 3) 返回 6。符号 "+" 与执行其参数加法的函数+相关联。(+ 1 (+ 2 3) 4) 返回 10。第二个参数包含一个表达式,并在被传递给外部+之前被执行。
+、-、*、/ 是对数字的基本操作。它们可以接受多个参数。请注意,(/ 1 2) 是 1/2,而不是 0.5 - Lisp 了解有理数(以及复数...)。<、<=、= 等用于数字比较。请注意,=、<、<= 等是多元的
(= 1 1 1) ⇒ t
(= 1 1 2) ⇒ nil
(< 1 2 3) ⇒ t
(< 1 3 2) ⇒ nil
list 顾名思义,创建了一个列表。
(list 1 2 3) ⇒ (1 2 3)
cons 创建了一个对(一对有 2 个元素,不是一个列表)。
(cons 1 2) ⇒ (1 . 2) ;;note the dot.
car 或 first 返回 cons(对)的第一个元素。cdr 或 rest 返回 cons 的第二个元素。
(car (cons 1 2)) ⇒ 1 (cdr (cons 1 2)) ⇒ 2
或者
(first '(1 2)) ⇒ 1
(second '(1 2)) ⇒ 2
由于列表在 Lisp 中非常突出,因此了解它们的本质很重要。事实是,除了一种例外,列表由 cons 组成。这个例外是一个称为nil的特殊列表 - 也称为()。nil是一个自执行的符号,既用作假值常量,又用作空列表。nil是 Lisp 中唯一的假值 - 对于if和类似构造来说,其他任何值都是真值。nil的反面是t,它也是自执行的,代表真值。然而,t不是一个列表。让我们回到列表...一个适当列表(不适当列表此处不作解释)被定义为任何列表,要么是nil,要么是一个cons,其cdr是适当列表。(请注意,由于适当列表必须从某个地方开始,(cdr (cdr (cdr... (cdr x)))...)
对于一些有限数量的cdr来说是nil。)
基本上,适当列表是一系列 cons,使得下一个 cons 是前一个 cons 的 cdr。如果您考虑一个图形表示,则很容易理解列表是如何构造的。cons 可以表示为一个被分成两个正方形的矩形。每个正方形可以容纳一个值。在适当的列表中,左边的正方形保存列表的元素,右边的正方形保存下一个 cons(如果它是列表的末尾,则保存nil)。请注意,每个 cons 仅保存列表中的一个元素。这就是 (1 2 3) 在图形表示中看起来的样子
.-------. | * | * | '-|---|-' V V 1 .-------. | * | * | '-|---|-' V V 2 .-------. | * | * | '-|---|-' V V 3 nil
所以 (1 2 3) 实际上是 (1 . (2 . (3 . nil)))。由此可知 (car (list 1 2 3)) 是 1,(cdr (list 1 2 3)) 是 (2 3)。从以上所有内容中得不出 (car nil) 和 (cdr nil) 是nil。这不太一致,因为 (cons nil nil) 与 nil 不同,但它碰巧很方便。
符号在其他编程语言中起着与变量名相同的作用。基本上,符号是一个与某些值关联的字符串。该字符串可以包含任何字符,包括空格和控制字符。但是,大多数符号名称不使用除字母、数字和连字符之外的字符,因为它们难以输入。此外,字符 "(", ")", "#", "\", ".", "|", ";", 空格以及双引号和单引号可能会被 Lisp 读取器误解;其他字符,如 "*" 通常仅用于某些目的。默认情况下,Lisp 将您的输入转换为大写。
符号在您使用它们时被创建。例如,当您输入 (setf x 1) 时,会创建名为 "X" 的符号(记住 Lisp 会将您的输入大写),并将它的值设置为 1。但是,在使用符号之前定义它们是良好的风格。defvar 和 defparameter 用于此目的。
(defparameter x 1) ;;defines symbol "X" and sets its value to 1.
符号还可以与它的名称和值之外的其他参数相关联 - 函数、类等等。要获取与符号关联的函数,可以使用一个特殊的运算符(这些将在下一章中讨论)function。
Lisp 中有一些运算符看起来像函数,但行为略有不同。这些是宏和特殊运算符。函数总是执行其参数,但这有时是不可取的,因此必须实现这些形式。
例如,考虑普遍存在的if构造。If 采用 (if condition then else) 的形式;首先执行condition,然后如果condition不是nil则执行then,如果condition是nil则执行else。因此,(if t 1 2) 返回 1,(if nil 1 2) 返回 2。显然,if 不能实现为函数,因为它的两个最终参数中只有一个会被执行。因此,它被创建为大约 25 个特殊运算符之一,这些运算符都在 Lisp 实现中预定义。
另一个特殊运算符是quote。它返回其唯一的参数,未执行。同样,这对于函数来说是不可能的,因为它们总是执行其参数。Quote 使用非常频繁,因此可以用单个字符 ' 表示。因此,(quote x) 等效于 'x。quote 可以用来快速创建列表:'(1 2 3) 返回 (1 2 3),'(x y z) 返回 (x y z) - 将其与 (list x y z) 进行比较,后者会创建 x、y 和 z 的值的列表,或者如果未分配任何值,则会发出错误信号。事实上,'(x y z) 与 (list 'x 'y 'z) 的值相同。
宏与特殊运算符类似,但它们不是在 Lisp 实现中硬编码的。相反,它们可以在 Lisp 代码中定义。您将使用的许多 Lisp 构造实际上是宏。只有非常基本的构造是硬编码的。当然,对于用户来说,没有区别。
本章将解释如何在 Lisp 中执行一些简单操作。我们将介绍许多有用的结构。阅读完本章后,您将能够编写简单的程序。
虽然在许多编程语言中,将值存储在变量中是一个重要的过程,但在 Lisp 中,它被使用的频率要低得多。尽管 Lisp 是一种多范式语言,但它通常被认为是一种函数式语言,并且也以函数式语言的方式进行编程。函数式语言不允许(或者至少不鼓励)使用状态,或者存储的信息会隐式地改变函数的行为。理论上,在纯函数式程序中,赋值永远不需要。您可能已经注意到,在上一章中,除了“符号”部分,我从未在任何地方存储过值。这表明,很少需要存储全局值。
话虽如此,存储值仍然有用,并且 Lisp 确实提供了这种功能。宏 setf 和 setq 将值存储到符号中。
(setq x 1) => 1
x => 1
(setq x 1 y 2 z 3) => 3
(list x y z) => (1 2 3)
Setf 比 setq 强大得多,因为它允许程序员更改变量的单个部分。
(setq abc '(1 2 3)) => (1 2 3)
(setq (car abc) 3) => error!
(setf (car abc) 3) => 3
abc => (3 2 3)
因此,setf 的使用频率比 setq 高。
在其他语言中,赋值通常表示为类似 x=1 的形式。这里的 = 与数学中的 = 符号含义不同。Lisp 将 = 保留用于数学定义,即测试数值相等性。它可能看起来像(setf place value)比必要的复杂,但记住,此功能是可扩展的,允许用户从赋值中删除数据的内部表示。这类似于在其他语言中重新分配 = 运算符(在 C 和一些其他语言中不可能做到)。
尽管 setf 需要额外的按键,但它仍然是一个有用的工具。但在实践中,您将比在其他语言中更少使用赋值,因为 Lisp 中还有另一种记住值的方法:通过绑定它们。
注意:setq 代表 set quote。最初,存在一个名为 set 的函数,它会评估其第一个参数。程序员厌倦了对参数进行引用,因此定义了这个特殊运算符。Set 现在已被弃用。
当值被绑定到符号时,它们被临时存储,然后解绑,值被遗忘。使用 let 和 let*,您可以在程序的某些部分将一些值绑定到一些变量。let 和 let* 之间的区别在于,let 并行初始化其变量,而 let* 则按顺序进行。
(let ((x 1) (y 2) (z 3)) (+ x y z)) => 6
(let* ((x 1) (y (+ x 1)) (z (+ y 1))) (+ x y z)) => 6
在 let 的主体内部,您可以像使用真实符号一样使用您定义的变量 - 在 let 的外部,这些符号可能被解绑,或者具有完全不同的值。如果您在 let 主体内部调用或定义函数,绑定会保留下来,从而使一些有趣的交互成为可能(这些交互超出了本手册的范围)。您甚至可以对这些变量使用 setf,并且新值将被临时存储。一旦 let 主体中的最后一个表单被执行,其结果将被返回,并且变量将恢复到其原始值。
(setf x 3 y 4 z 5) => 5
(let ((x 1) (y 2) (z 3)) (+ x y z)) => 6
(+ x y z) => 12
良好的编程实践通常建议尽可能使用局部变量,并且仅在绝对必要时才使用全局变量。因此,您应该尽可能使用 let,并将 setf 视为每使用一次都要缴税。
if 运算符之前已经解释过,但在此时可能看起来很难使用。这是因为 if 每个分支只允许一个表单,这使得它在大多数情况下难以使用。幸运的是,Lisp 语法为您提供了比 C 的花括号或 Pascal 的 begin/end 更大的自由来定义块。progn 创建了一个非常简单的代码块,依次执行其参数,并返回最后一个参数的结果。
(progn (setf x 1 y 2) (setf z (+ x y)) (* y z)) => 6
let 和 let* 也可以用于此目的,尤其是在您希望在分支内部使用一些临时变量时。
block 创建一个命名块,您可以使用 return-from 从该块返回。
(block aaa
(return-from aaa 1)
(+ 1 2 3)) => 1 ;;The form (+ 1 2 3) is not evaluated.
......还存在其他表单,例如 the、locally、prog1、tagbody 等等。幸运的是,如果您不喜欢编写 Lisp 宏,if 可能是唯一需要使用块的结构。
由于 if 在重复使用时非常难看,所以有一些方便的宏可以使用,这样您就不必经常使用它。when 评估其第一个参数,如果它不是 nil,则评估其其余参数,并返回最后一个结果。unless 在其第一个参数为 nil 时执行相同的操作。否则,它们都返回 nil。
cond 稍微复杂一些,但也有用得多。它测试其条件,直到其中一个条件不为 nil,然后评估关联的代码。这比嵌套的 if 更易于解析,并且外观更好。cond 的语法如下:
(cond (condition1 some-forms1)
(condition2 some-forms2)
...and so on...)
case 与 cond 类似,但它在检查某个表达式的值后进行分支。
(case expression
(values1 some-forms1) ;values is either one value
(values2 some-forms2) ;or a list of values.
...
(t some-forms-t)) ;executed if no values match
or 评估其参数,直到其中一个参数不为 nil,返回其值,或者返回最后一个参数的值。
and 评估其参数,直到其中一个参数为 nil,返回其值(即 nil) - 否则返回最后一个参数的值。
您可能会注意到,or 和 and 也可以用作逻辑运算 - 请记住,所有非 nil 的值都为真。
迭代,或者多次评估一个表单,由许多工具来完成。最实用的工具是 loop - 在其最简单的形式中,它会简单地执行其主体,直到调用 return 运算符,在这种情况下,它会返回指定的值。
(setf x 0)
(loop (setf x (+ x 1)) (when (> x 10) (return x))) => 11
loop 的更复杂形式最好通过示例来学习。根据所需的迭代类型,将使用不同的运算符,例如 for 和 until。虽然 loop 应该足以用于所有类型的循环,但对于那些不喜欢学习其完整语法的人来说,还有一些其他结构。
dotimes 会执行一些代码固定次数。其语法是
(dotimes (var number result)
forms)
dotimes 将 var 从 0 递增到 number,并每次使用 var 的该值执行 forms,最后返回 result。
dolist 遍历列表:其语法与 dotimes 相同,只是列表替换了 number。var 从列表的 car 开始,并移动直到它到达列表的最后一个元素。
mapcar 将函数应用于不同的参数集,并返回结果列表,例如
(mapcar #'+ '(1 3 6) '(2 4 7) '(3 5 8)) => (6 12 21)
#'+ 是 (function +) 的快捷方式。函数 + 首先应用于参数列表 (1 2 3),然后应用于 (3 4 5),最后应用于 (6 7 8)。
函数使用宏 defun 定义
(defun function-name (arguments)
(body))
创建的函数与符号 function-name 相关联,并且可以像任何其他函数一样被调用。值得一提的是,以这种方式定义的函数可以是递归的,也可以互相调用 - 这是大多数 Lisp 编程中必不可少的组成部分。递归函数的示例是
(defun factorial (x)
(if (= x 0) 1
(* x (factorial (- x 1)))))
如您所见,此函数只是通过反复调用自身来描述一个原本复杂的运算。当 x = 0 时,该过程停止,并且 x 在每次调用时都减小,消除了(对于正整数 x)无限循环的可能性。请注意,x 作为参数,在每次调用函数时都具有不同的值。如上文“绑定值”中所述,这些值都不会覆盖之前的任何值。
特殊运算符 lambda 创建一个匿名函数,您可以将其用于一次性目的。其语法相同,只是将“defun function-name”替换为“lambda”。这些函数不能是递归的。在大多数情况下,lambda 函数用于 mapcar(以及下面的 funcall 和 apply)等表单中,以消除过多的重复或内存使用。
您还可以使用 flet 和 labels 临时绑定函数。它们与 let 和 let* 非常相似。它们之间的区别在于,在 labels 中,函数可以引用自身,而在 flet 中,它引用具有相同名称的先前函数。
要调用具有参数 a1、a2 和 a3 的函数 f,只需键入
(f a1 a2 a3)
有时,要调用的函数存储在变量中,并且您事先不知道其名称。或者,您可能不知道要传递多少参数。在这些情况下,函数 funcall 和 apply 就派上用场了。就像所有函数一样,它们会评估其参数 - 第一个参数应该生成要调用的函数,其余参数应该生成参数。Funcall 只是使用提供的任何参数调用函数。Apply 检查其最后一个参数是否为列表,如果是,则将其视为参数列表。比较
(funcall #'list '(1 2) '(3 4)) => ((1 2) (3 4))
(apply #'list '(1 2) '(3 4)) => ((1 2) 3 4)
在本教程中,我只介绍了简单的输入和输出任务。要从用户那里读取值,请使用 **read** 函数。它将尝试从输入中读取一个任意的 Lisp 表达式,而 **read** 返回的值就是该表达式。
>(read) ;Run read function
(+ 1 x) ;That's what the user types
(+ 1 X) ;that's what is returned
在这个例子中,返回值是一个包含三个元素的列表:符号 +、数字 1 和符号 X(注意它被大写了)。**read** 是构成 *read-eval-print* 循环的三个函数之一,它是 Lisp 的核心元素。这与您在 Lisp 提示符下键入的表达式所使用的函数相同。
虽然 **read** 对接收用户输入的数字和列表非常方便,但大多数用户期望以不同的方式将其他数据类型提供给计算机。例如,字符串。为了让 **read** 识别字符串,必须在它周围添加双引号 `“like this”`。但是,普通用户期望直接输入 `like this` 而不带引号,然后按回车键。因此,有一个不同的通用输入函数:**read-line**。**read-line** 返回一个字符串,其中包含用户在按回车键之前输入的内容。然后,您可以处理该字符串以提取所需的信息。
对于输出,也有相当多的可用函数。我们感兴趣的是 **princ**,它只打印提供的值,并返回它。这在 Lisp 控制台中使用时可能会令人困惑。
>(princ "aaa")
aaa
"aaa"
第一个 aaa(不带引号)是打印的内容,而第二个是返回值(由 **print** 函数打印,它看起来不太好看)。另一个可能很有用的函数是 **terpri**,它在输出中打印一个新行(名称 "terpri" 是历史性的,意思是 "TERminate PRInt line")。
有关更多信息,请返回 Common Lisp。