特殊形式

讲解 if 与 cond 特殊形式的结构与用法、特殊形式不求值实参的特性,以及 cond 的括号陷阱。

if 子句

尽管我们在上一课已经做过一些使用 if 的练习,这里还是给出这个特殊形式的一般结构:

(if [test]
    [then]
    [else])

if 是特殊形式(special form),因为除非使用它,它不会求值自己的实参。下面看几个例子:

-> (if #t
       'foo
       'baz)
'foo
-> (if #f
       'foo
       'baz)
'baz
-> (if (= 1 1)
       'foobar
       (/ 1 0))
'foobar

最后一个例子说明了 if 为什么需要是特殊形式。由于 (= 1 1) 求值为 #t,我们永远走不到 else 分支 (/ 1 0),成功返回了 'foobar。

cond 子句

可以在 if 表达式中嵌套 if 表达式,像这样:

(define (roman-value letter)
  (if (equal? letter 'i)
      1
      (if (equal? letter 'v)
          5
          (if (equal? letter 'x)
              10
              (if (equal? letter 'l)
                  50
                  (if (equal? letter 'c)
                      100
                      (if (equal? letter 'd)
                          500
                          (if (equal? letter 'm)
                              1000
                              'huh?)))))))))

对于有很多分支的条件判断,这样写很有用。但是,分支越多,代码就越乱、可读性越差。嵌套 if 有一种简写形式,就是 cond 子句,它用不同的语法完成相同的任务。下面是用 cond 语句重写的 roman-value 函数:

(define (roman-value letter)
  (cond ((equal? letter 'i) 1)
        ((equal? letter 'v) 5)
        ((equal? letter 'x) 10)
        ((equal? letter 'l) 50)
        ((equal? letter 'c) 100)
        ((equal? letter 'd) 500)
        ((equal? letter 'm) 1000)
        (else 'huh?)))

如你所见,cond 子句让你可以依次指定一系列条件和对应的取值。末尾的 else 子句指定当前面所有谓词都不为真时返回的值。

把上面的代码翻译成中文,读起来是这样的:

  • 如果输入字母是 “i”,值是 1。
  • 如果输入字母是 “v”,值是 5。
  • ……
  • 如果输入字母是 “m”,值是 1000。
  • 否则,当以上条件都不成立时,值是 'huh?。

cond 子句的一般结构如下:

(cond ([test1] [then1])
      ([test2] [then2])
      ...
      ([testn] [thenn])
      (else [else]))

特殊形式

特殊形式是不遵循正常求值步骤的过程。我们之前学过,表达式中的所有实参都会在过程应用到实参之前被求值。特殊形式则不然。在目前已讲的谓词和子句中,if、cond、or 和 and 都是特殊形式。

检验理解

下面的表达式目前会报错。重新安排它们的实参顺序,使表达式不再报错并返回正确的值。不要改动任何实参的值。

(and (/ 1 0) #f #t)
(or #f (/ 1 0) #t)

假设我们决定自己写一个 if 过程,名为 new-if,定义如下:

(define (new-if test then else)
  (if test
      then
      else))

它应该和 if 完全一样,因为它只是在过程体里调用 if。但是,由于这是一个复合过程,它不是特殊形式。当我们这样调用 new-if 时会发生什么?

(new-if (= 1 1) 'foo (/ 1 0))

由于 new-if 不是特殊形式,它会先求值所有实参,然后才进入过程体。

  1. (= 1 1) 返回 #t
  2. 'foo 返回 'foo
  3. (/ 1 0) 返回——等一下……

而由于 (/ 1 0) 会报错,我们复刻 if 特殊形式的尝试失败了。

if 是可组合的

为节省时间和代码篇幅,请记住:像 if 和 cond 这样的函数可以用在表达式内部,而不必单独成为表达式。为演示这一点,考虑下面这个简单的函数:

(define (what-am-i age)
  (if (> age 21)
      '(i am a grownup)
      '(i am a child)))

我们可以把它改写成这样:

(define (what-am-i age)
  (se '(i am a) (if (> age 21)
                    'grownup
                    'child)))

诚然,这样看起来差别不大。考虑到这是个简单函数,这完全可以理解。当这种技巧用在更复杂的函数中时,我们通过避免重复来节省时间。从上面可以看到,改写后的函数只写了 '(i am a) 一次,而原来的定义写了两遍。

易错点

cond 语句的结构对括号有非常严格的规定。如果你的代码报错,很可能是你少了一对括号或多加了一对。因此,使用 COND 语句时一定要注意括号!

另一个问题是,and 和 or 不能像英语里那样使用。具体来说,假设我们有一个表达式,想检查某个实参是 'yes 还是 'no:

(equal? argument (or 'yes 'no))

这样写是错的。or 返回第一个不为假的实参,因此在这个例子中它会返回 'yes。这个表达式最终会被求值为:

(equal? argument 'yes)

如果你想检查实参是 'yes 还是 'no,你需要这样写:

(or (equal? argument 'yes) (equal? argument 'no))

最后,但绝非最不重要的一点:一定要避免冗余代码。简单的代码才是聪明的代码,它会让复杂的程序更易读、更易操作。

冗余代码的例子:

(define (even? number)
  (if (not (odd? number))
      #t
      #f))

这是糟糕的代码风格。我们可以把它简化为一行:

(define (even? number)
  (not (odd? number)))

检验理解

注意: 这是作业中的练习 3。

写一个过程 indef-article,只接收一个词作为实参,返回一个句子。关于 indef-article 应该怎么工作,见下面的例子。记住,以辅音开头的事物的不定冠词是 “a”,以元音开头的是 “an”。你可以忽略任何边缘情况。

-> (indef-article 'beetle)
'(a beetle)
-> (indef-article 'apple)
'(an apple)