《ANSI Common Lisp》- 第五章:控制流【笔记】

本章的操作符都有一个共同点,就是它们都违反了求值规则。这些操作符让你决定在程序当中何时要求值。如果普通的函数调用是 Lisp 程序的树叶的话,那这些操作符就是连结树叶的树枝。

5.1 区块 (Blocks)

Common Lisp 有三个构造区块(block)的基本操作符: progn 、 block 以及 tagbody 。我们已经看过 progn 了。在 progn 主体中的表达式会依序求值,并返回最后一个表达式的值:

CL-USER> (progn

    (format t "a")

    (format t "b")

    (+ 11 12))

ab

23

由于只返回最后一个表达式的值,代表着使用 progn (或任何区块)涵盖了副作用。

一个 block 像是带有名字及紧急出口的 progn 。第一个实参应为符号。这成为了区块的名字。在主体中的任何地方,可以停止求值,并通过使用 return-from 指定区块的名字,来立即返回数值:

CL-USER> (block head

    (format t "Here we go.")

    (return-from head 'idea)

    (format t "We'll never see this."))

Here we go.

IDEA

调用 return-from 允许你的程序,从代码的任何地方,突然但优雅地退出。第二个传给 return-from 的实参,用来作为以第一个实参为名的区块的返回值。在return-from 之后的表达式不会被求值。

也有一个 return 宏,它把传入的参数当做封闭区块 nil 的返回值:

CL-USER> (block nil

    (return 27))

27

许多接受一个表达式主体的 Common Lisp 操作符,皆隐含在一个叫做 nil 的区块里。比如,所有由 do 构造的迭代函数:

CL-USER>(dolist (x '(a b c d e))

    (format t "~A " x)

    (if (eql x 'c)

        (return 'done)))

A B C

DONE

使用 defun 定义的函数主体,都隐含在一个与函数同名的区块,所以你可以:

CL-USER> (defun foo ()

  (return-from foo 27))

FOO

在一个显式或隐式的 block 外,不论是 return-from 或 return 都不会工作。

使用 return-from ,我们可以写出一个更好的 read-integer 版本:

(defun read-integer (str)

  (let ((accum 0))

    (dotimes (pos (length str))

      (let ((i (digit-char-p (char str pos))))

        (if i

            (setf accum (+ (* accum 10) i))

            (return-from read-integer nil))))

    accum))

68 页的版本在构造整数之前,需检查所有的字符。现在两个步骤可以结合,因为如果遇到非数字的字符时,我们可以舍弃计算结果。出现在主体的原子(atom)被解读为标签(labels);把这样的标签传给 go ,会把控制权交给标签后的表达式。以下是一个非常丑的程序片段,用来印出一至十的数字:

> (tagbody

    (setf x 0)

    top

      (setf x (+ x 1))

      (format t "~A " x)

      (if (< x 10) (go top)))

1 2 3 4 5 6 7 8 9 10

NIL

这个操作符主要用来实现其它的操作符,不是一般会用到的操作符。大多数迭代操作符都隐含在一个 tagbody ,所以是可能可以在主体里(虽然很少想要)使用标签及 go 。

如何决定要使用哪一种区块建构子呢(block construct)?几乎任何时候,你会使用 progn 。如果你想要突然退出的话,使用 block 来取代。多数程序员永远不会显式地使用 tagbody 。

5.2 语境 (Context)

另一个我们用来区分表达式的操作符是 let 。它接受一个代码主体,但允许我们在主体内设置新变量:

CL-USER> (let ((x 7)

        (y 2))

    (format t "Number")

    (+ x y))

Number

9

一个像是 let 的操作符,创造出一个新的词法语境(lexical context)。在这个语境里有两个新变量,然而在外部语境的变量也因此变得不可视了。

概念上说,一个 let 表达式等同于函数调用。在 2.14 节证明过,函数可以用名字来引用,也可以通过使用一个 lambda 表达式从字面上来引用。由于 lambda 表达式是函数的名字,我们可以像使用函数名那样,把 lambda 表达式作为函数调用的第一个实参:

CL-USER> ((lambda (x) (+ x 1)) 3)

4

前述的 let 表达式,实际上等同于:

CL-USER> ((lambda (x y)

  (format t "Number")

  (+ x y))

7

2)

Number

9

如果有关于 let 的任何问题,应该是如何把责任交给 lambda ,因为进入一个 let 等同于执行一个函数调用。

这个模型清楚的告诉我们,由 let 创造的变量的值,不能依赖其它由同一个 let 所创造的变量。举例来说,如果我们试着:

(let ((x 2)

      (y (+ x 1)))

  (+ x y))

在 (+ x 1) 中的 x 不是前一行所设置的值,因为整个表达式等同于:

((lambda (x y) (+ x y)) 2

                        (+ x 1))

这里明显看到 (+ x 1) 作为实参传给函数,不能引用函数内的形参 x 。

所以如果你真的想要新变量的值,依赖同一个表达式所设立的另一个变量?在这个情况下,使用一个变形版本 let* :

CL-USER> (let* ((x 1)

        (y (+ x 1)))

    (+ x y))

3

一个 let* 功能上等同于一系列嵌套的 let 。这个特别的例子等同于:

CL-USER> (let ((x 1))

  (let ((y (+ x 1)))

    (+ x y)))

3

let 与 let* 将变量初始值都设为 nil 。nil 为初始值的变量,不需要依附在列表内:

CL-USER> (let (x y)

    (list x y))

(NIL NIL)

destructuring-bind 宏是通用化的 let 。其接受单一变量,一个模式 (pattern) ── 一个或多个变量所构成的树 ── 并将它们与某个实际的树所对应的部份做绑定。举例来说:

CL-USER> (destructuring-bind (w (x y) . z) '(a (b c) d e)

    (list w x y z))

(A B C (D E))

若给定的树(第二个实参)没有与模式匹配(第一个参数)时,会产生错误。

5.3 条件 (Conditionals)

最简单的条件式是 if ;其余的条件式都是基于 if 所构造的。第二简单的条件式是 when ,它接受一个测试表达式(test expression)与一个代码主体。若测试表达式求值返回真时,则对主体求值。所以

(when (oddp that)

  (format t "Hmm, that's odd.")

  (+ that 1))

等同于

(if (oddp that)

    (progn

      (format t "Hmm, that's odd.")

      (+ that 1)))

when 的相反是 unless ;它接受相同的实参,但仅在测试表达式返回假时,才对主体求值。

所有条件式的母体 (从正反两面看) 是 cond , cond 有两个新的优点:允许多个条件判断,与每个条件相关的代码隐含在 progn 里。 cond 预期在我们需要使用嵌套 if 的情况下使用。 举例来说,这个伪 member 函数

(defun our-member (obj lst)

  (if (atom lst)

      nil

      (if (eql (car lst) obj)

          lst

          (our-member obj (cdr lst)))))

也可以定义成:

(defun our-member (obj lst)

  (cond ((atom lst) nil)

        ((eql (car lst) obj) lst)

        (t (our-member obj (cdr lst)))))

事实上,Common Lisp 实现大概会把 cond 翻译成 if 的形式。

总得来说呢, cond 接受零个或多个实参。每一个实参必须是一个具有条件式,伴随着零个或多个表达式的列表。当 cond 表达式被求值时,测试条件式依序求值,直到某个测试条件式返回真才停止。当返回真时,与其相关联的表达式会被依序求值,而最后一个返回的数值,会作为 cond 的返回值。如果符合的条件式之后没有表达式的话:

CL-USER> (cond (99))

99

则会返回条件式的值。

由于 cond 子句的 t 条件永远成立,通常我们把它放在最后,作为缺省的条件式。如果没有子句符合时,则 cond 返回 nil ,但利用 nil 作为返回值是一种很差的风格 (这种问题可能发生的例子,请看 292 页)。译注: Appendix A, unexpected nil 小节。

当你想要把一个数值与一系列的常量比较时,有 case 可以用。我们可以使用 case 来定义一个函数,返回每个月份中的天数:

(defun month-length (mon)

  (case mon

    ((jan mar may jul aug oct dec) 31)

    ((apr jun sept nov) 30)

    (feb (if (leap-year) 29 28))

    (otherwise "unknown month")))

一个 case 表达式由一个实参开始,此实参会被拿来与每个子句的键值做比较。接着是零个或多个子句,每个子句由一个或一串键值开始,跟随着零个或多个表达式。键值被视为常量;它们不会被求值。第一个参数的值被拿来与子句中的键值做比较 (使用 eql )。如果匹配时,子句剩余的表达式会被求值,并将最后一个求值作为 case 的返回值。

缺省子句的键值可以是 t 或 otherwise 。如果没有子句符合时,或是子句只包含键值时,

CL-USER> (case 99 (99))

NIL

则 case 返回 nil 。

typecase 宏与 case 相似,除了每个子句中的键值应为类型修饰符 (type specifiers),以及第一个实参与键值比较的函数使用 typep 而不是 eql (一个typecase 的例子在 107 页)。 译注: 6.5 小节。

5.4 迭代 (Iteration)

最基本的迭代操作符是 do ,在 2.13 小节介绍过。由于 do 包含了隐式的 block 及 tagbody ,我们现在知道是可以在 do 主体内使用 return 、 return-from 以及 go 。

2.13 节提到 do 的第一个参数必须是说明变量规格的列表,列表可以是如下形式:

(variable initial update)

initial 与 update 形式是选择性的。若 update 形式忽略时,每次迭代时不会更新变量。若 initial 形式也忽略时,变量会使用 nil 来初始化。

在 23 页的例子中(译注: 2.13 节),

(defun show-squares (start end)

  (do ((i start (+ i 1)))

      ((> i end) 'done)

    (format t "~A ~A~%" i (* i i))))

update 形式引用到由 do 所创造的变量。一般都是这么用。如果一个 do 的 update 形式,没有至少引用到一个 do 创建的变量时,反而很奇怪。

当同时更新超过一个变量时,问题来了,如果一个 update 形式,引用到一个拥有自己的 update 形式的变量时,它会被更新呢?或是获得前一次迭代的值?使用do 的话,它获得后者的值:

CL-USER> (let ((x 'a))

    (do ((x 1 (+ x 1))

        (y x x))

        ((> x 5))

      (format t "(~A ~A)  " x y)))

(1 A)  (2 1)  (3 2)  (4 3)  (5 4) 

NIL

每一次迭代时, x 获得先前的值,加上一; y 也获得 x 的前一次数值。

但也有一个 do* ,它有着和 let 与 let* 一样的关系。任何 initial 或 update 形式可以参照到前一个子句的变量,并会获得当下的值:

CL-USER> (do* ((x 1 (+ x 1))

      (y x x))

    ((> x 5))

  (format t "(~A ~A) " x y))

(1 1) (2 2) (3 3) (4 4) (5 5)

NIL

除了 do 与 do* 之外,也有几个特别用途的迭代操作符。要迭代一个列表的元素,我们可以使用 dolist :

CL-USER> (dolist (x '(a b c d) 'done)

    (format t "~A " x))

A B C D

DONE

当迭代结束时,初始列表内的第三个表达式 (译注: done ) ,会被求值并作为 dolist 的返回值。缺省是 nil 。

有着同样的精神的是 dotimes ,给定某个 n ,将会从整数 0 ,迭代至 n-1 :

CL-USER> (dotimes (x 5 x)

  (format t "~A " x))

0 1 2 3 4

5

dolist 与 dotimes 初始列表的第三个表达式皆可省略,省略时为 ``nil 。注意该表达式可引用到迭代过程中的变量。

(译注:第三个表达式即上例之 x ,可以省略,省略时 dotimes 表达式的返回值为 nil 。)

do 的重点 (THE POINT OF do)

在 “The Evolution of Lisp” 里,Steele 与 Garbriel 陈述了 do 的重点, 表达的实在太好了,值得整个在这里引用过来:

撇开争论语法不谈,有件事要说明的是,在任何一个编程语言中,一个循环若一次只能更新一个变量是毫无用处的。 几乎在任何情况下,会有一个变量用来产生下个值,而另一个变量用来累积结果。如果循环语法只能产生变量, 那么累积结果就得借由赋值语句来“手动”实现…或有其他的副作用。具有多变量的 do 循环,体现了产生与累积的本质对称性,允许可以无副作用地表达迭代过程:

(defun factorial (n)

  (do ((j n (- j 1))

      (f 1 (* j f)))

    ((= j 0) f)))

当然在 step 形式里实现所有的实际工作,一个没有主体的 do 循环形式是较不寻常的。

函数 mapc 和 mapcar 很像,但不会 cons 一个新列表作为返回值,所以使用的唯一理由是为了副作用。它们比 dolist 来得灵活,因为可以同时遍历多个列表:

CL-USER> (mapc #'(lambda (x y)

          (format t "~A ~A  " x y))

      '(hip flip slip)

      '(hop flop slop))

HIP HOP  FLIP FLOP  SLIP SLOP 

(HIP FLIP SLIP)

总是返回 mapc 的第二个参数。

5.5 多值 (Multiple Values)

曾有人这么说,为了要强调函数式编程的重要性,每个 Lisp 表达式都返回一个值。现在事情不是这么简单了;在 Common Lisp 里,一个表达式可以返回零个或多个数值。最多可以返回几个值取决于各家实现,但至少可以返回 19 个值。

多值允许一个函数返回多件事情的计算结果,而不用构造一个特定的结构。举例来说,内置的 get-decoded-time 返回 9 个数值来表示现在的时间:秒,分,时,日期,月,年,天,以及另外两个数值。

多值也使得查询函数可以分辨出 nil 与查询失败的情况。这也是为什么 gethash 返回两个值。因为它使用第二个数值来指出成功还是失败,我们可以在哈希表里储存 nil ,就像我们可以储存别的数值那样。

values 函数返回多个数值。它一个不少地返回你作为数值所传入的实参:

CL-USER> (values 'a nil (+ 2 4))

A

NIL

6

如果一个 values 表达式,是函数主体最后求值的表达式,它所返回的数值变成函数的返回值。多值可以原封不地通过任何数量的返回来传递:

CL-USER> ((lambda () ((lambda () (values 1 2)))))

1

2

然而若只预期一个返回值时,第一个之外的值会被舍弃:

CL-USER> (let ((x (values 1 2)))

    x)

1

通过不带实参使用 values ,是可能不返回值的。在这个情况下,预期一个返回值的话,会获得 nil :

> (let ((x (values)))

    x)

NIL

要接收多个数值,我们使用 multiple-value-bind :

CL-USER> (multiple-value-bind (x y z) (values 1 2 3)

    (list x y z))

(1 2 3)

CL-USER> (multiple-value-bind (x y z) (values 1 2)

    (list x y z))

(1 2 NIL)

如果变量的数量大于数值的数量,剩余的变量会是 nil 。如果数值的数量大于变量的数量,多余的值会被舍弃。所以只想印出时间我们可以这么写:

CL-USER> (multiple-value-bind (s m h) (get-decoded-time)

    (format t "~A:~A:~A" h m s))

10:5:45

NIL

你可以借由 multiple-value-call 将多值作为实参传给第二个函数:

CL-USER> (multiple-value-call #'+ (values 1 2 3))

6

还有一个函数是 multiple-value-list :

CL-USER> (multiple-value-list (values 'a 'b 'c))

(A B C)

看起来像是使用 #'list 作为第一个参数的来调用 multiple-value-call 。

5.6 中止 (Aborts)

你可以使用 return 在任何时候离开一个 block 。有时候我们想要做更极端的事,在数个函数调用里将控制权转移回来。要达成这件事,我们使用 catch 与throw 。一个 catch 表达式接受一个标签(tag),标签可以是任何类型的对象,伴随着一个表达式主体:

CL-USER> (defun super ()

  (catch 'abort

    (sub)

CL-USER> (format t "We'll never see this.")))

(defun sub ()

  (throw 'abort 99))

表达式依序求值,就像它们是在 progn 里一样。在这段代码里的任何地方,一个带有特定标签的 throw 会导致 catch 表达式直接返回:

CL-USER> (super)

99

一个带有给定标签的 throw ,为了要到达匹配标签的 catch ,会将控制权转移 (因此杀掉进程)给任何有标签的 catch 。如果没有一个 catch 符合欲匹配的标签时, throw 会产生一个错误。

调用 error 同时中断了执行,本来会将控制权转移到调用树(calling tree)的更高点,取而代之的是,它将控制权转移给 Lisp 错误处理器(error handler)。通常会导致调用一个中断循环(break loop)。以下是一个假定的 Common Lisp 实现可能会发生的事情:

[3]> (progn

    (error "Oops!")

    (format t "After the error."))

*** - Oops!

The following restarts are available:

ABORT          :R1      Abort main loop

Break 1 [4]>

原文的实现

关于错误与状态的更多讯息,参见 14.6 小节以及附录 A。

有时候你想要防止代码被 throw 与 error 打断。借由使用 unwind-protect ,可以确保像是前述的中断,不会让你的程序停在不一致的状态。一个 unwind-protect 接受任何数量的实参,并返回第一个实参的值。然而即便是第一个实参的求值被打断时,剩下的表达式仍会被求值:

[5]> (setf x 1)

1

[6]> (catch 'abort

    (unwind-protect

      (throw 'abort 99)

      (setf x 2)))

99

[7]> x

2

在这里,即便 throw 将控制权交回监测的 catch , unwind-protect 确保控制权移交时,第二个表达式有被求值。无论何时,一个确切的动作要伴随着某种清理或重置时, unwind-protect 可能会派上用场。在 121 页提到了一个例子。

5.7 示例:日期运算 (Example: Date Arithmetic)

Chapter 5 总结 (Summary)

1)Common Lisp 有三个基本的区块建构子: progn ;允许返回的 block ;以及允许 goto 的 tagbody 。很多内置的操作符隐含在区块里。

2)进入一个新的词法语境,概念上等同于函数调用。

3)Common Lisp 提供了适合不同情况的条件式。每个都可以使用 if 来定义。

4)有数个相似迭代操作符的变种。

5)表达式可以返回多个数值。

6)计算过程可以被中断以及保护,保护可使其免于中断所造成的后果。

笔记:

一、常用函数

block:一个 block 像是带有名字及紧急出口的 progn 。第一个实参应为符号。这成为了区块的名字。在主体中的任何地方,可以停止求值,并通过使用 return-from 指定区块的名字,来立即返回数值.

return-from:调用 return-from 允许你的程序,从代码的任何地方,突然但优雅地退出。第二个传给 return-from 的实参,用来作为以第一个实参为名的区块的返回值。在return-from 之后的表达式不会被求值。

return:它把传入的参数当做封闭区块 nil 的返回值。

dolist:迭代一个列表的元素。当迭代结束时,初始列表内的第三个表达式  ,会被求值并作为 dolist 的返回值。缺省是 nil 。

dotimes:给定某个 n ,将会从整数 0 ,迭代至 n-1。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容