在本页中:
2.2.1 定义
2.2.2 关于代码缩进的提示
2.2.3 标识符
2.2.4 函数调用
2.2.5 条件分支与 ifandorcond
2.2.6 再谈函数调用
2.2.7 匿名函数与 lambda
2.2.8 局部绑定与 defineletlet*

2.2 简单的定义与表达式

程序模块的形式为:

#lang langname topform*

其中 topformdefinitionexpr 二者之一。REPL 也会对 topform 进行求值。

在语法规范中,灰色背景的文本(如 #lang)表示文本字面量。 文本字面量和 id 之类的非终止符(Nonterminals)之间必须有空格; 在 ()[] 的前后则不必有空格。

+The Racket ReferenceReading Comments一节中提供了关于注释的不同形式的更多信息。

按照约定,文法中的 * 表示前一元素的零次或多次重复,+ 表示前一元素的一次或多次重复,而 {} 则对一个序列进行分组,以作为一个元素用于重复。

2.2.1 定义

定义(Definition)的形式为:

+定义:define一节(本手册后文)中解释了有关 定义 的更多详情。

( define id expr )

它将 id 绑定到 expr 的结果上。而

( define ( id id* ) expr+ )

则将第一个 id 绑定到一个函数(亦称为过程)上,该函数接受以其余 id 为名的参数。对于这种函数的情况,expr 即为其函数体。 当该函数被调用时,它返回最后一个 expr 的结果。

例如:
(define pie 3)             ; pie 定义为 3
(define (piece str)        ; piece 定义为
  (substring str 0 pie))   ; 接受一个参数的函数
> pie

3

> (piece "key lime")

"key"

在底层,函数定义与非函数定义其实是相同的。而在函数调用中,函数名也不是必需的。 函数只是另一种值,虽然其打印的形式肯定没有数值或字符串那么完整。

例如:
> piece

#<procedure:piece>

> substring

#<procedure:substring>

函数定义的函数体中可包含多个表达式。在此情况下,当该函数被调用时, 只有最后一个表达式的值会被返回。其它表达式的求值仅用于某些副作用 (side-effect),例如打印等。

例如:
(define (bake flavor)
  (printf "preheating oven...\n")
  (string-append flavor " pie"))
> (bake "apple")

preheating oven...

"apple pie"

Racket 程序员更倾向于避免副作用,因此定义体(Definition Body)中通常只有一个表达式。 不过,了解定义体中允许多个表达式也很重要,因为它解释了下面的 nobake 函数在将其参数包含在结果中时为何会失败:

(define (nobake flavor)
  string-append flavor "jello")

 

> (nobake "green")

"jello"

nobake 中,string-append flavor "jello" 没有被括号括住, 因此该函数体中有三个独立的表达式,而非只有一个函数调用表达式。虽然表达式 string-appendflavor 会被求值,但它们的结果永远不会被使用。 而该函数的结果只不过是最后一个表达式的结果,即 "jello"

2.2.2 关于代码缩进的提示

换行和缩进对于 Racket 程序的解析来说并不重要,不过大部分 Racket 程序员都会使用一套标准的约定来提高代码可读性。例如,定义体通常在该定义的第一行下方开始缩进。 标识符(Identifiers)通常紧跟在开括号之后,中间没有额外的空格,而闭括号则从不独占一行。

当你在程序或 REPL 中敲 Enter 键时,DrRacket 会按照标准风格自动缩进。 例如,如果你在键入 (define (greet name) 后敲 Enter 键,那么 DrRacket 会自动为下一行插入两个空格。如果你要更改一片区域内的代码,可以在 DrRacket 中选中它们后敲 Tab 键,DrRacket 会重新缩进这片代码(但不会插入任何换行)。 Emacs 这类的编辑器提供了 Racket 或 Scheme 模式,它们支持类似的缩进方式。

重新缩进不仅能让你的代码更加易读,它还能提供额外的反馈,例如括号是否按预期匹配。 例如,如果你在函数的最后一个参数之后遗漏了一个闭括号括号, 那么自动缩进会在第一个参数的正下方开始下一行,而不会在 define 关键字下开始:

(define (halfbake flavor
                  (string-append flavor " creme brulee")))

在这种情况下,缩进有助于突出错误。在其它情况下,当一个开括号没有匹配的闭括号时, 缩进的地方可能是正常的,racket 和 DrRacket 会使用源码的缩进来提示可能缺少括号的地方。

2.2.3 标识符

Racket 标识符的语法相当自由。除特殊字符

   ( ) [ ] { } " , ' ` ; # | \

和构成数值常量的字符序列外,几乎任何非空白字符序列都能构成 id。 例如 substring 就是个标识符。string-appenda+b 也是标识符,而非算术表达式。下面还有一些示例:

+
Hfuhruhurr
integer?
pass/fail
john-jacob-jingleheimer-schmidt
a-b-c+1-2-3
2.2.4 函数调用(过程应用)

我们已经见过一些函数调用了,更传统的术语称之为过程应用。函数调用的语法为:

+函数调用一节(本手册后文)中解释了有关 函数调用 的更多详情。

( id expr* )

其中 expr 的数量决定了提供给名为 id 的函数的参数个数。

racket 语言预定义了很多函数标识符,例如 substringstring-append。更多示例见下。

Racket 示例代码贯穿了整个文档,其中用到的预定义名被超链接到了参考手册(Reference Manual)。 因此,你可以点击标识符来获取关于其用法的完整详情。

> (string-append "rope" "twine" "yarn")  ; 连接字符串

"ropetwineyarn"

> (substring "corduroys" 0 4)            ; 提取子字符串

"cord"

> (string-length "shoelace")             ; 获取字符串长度

8

> (string? "Ceci n'est pas une string.") ; 识别字符串

#t

> (string? 1)

#f

> (sqrt 16)                              ; 查找平方根

4

> (sqrt -16)

0+4i

> (+ 1 2)                                ; 数值相加

3

> (- 2 1)                                ; 数值相减

1

> (< 2 1)                                ; 数值比较

#f

> (>= 2 1)

#t

> (number? "c'est une number")           ; 识别数字

#f

> (number? 1)

#t

> (equal? 6 "half dozen")                ; 比较相等性

#f

> (equal? 6 6)

#t

> (equal? "half dozen" "half dozen")

#t

2.2.5 条件分支与 ifandorcond

下一种最简单的表达式为 if 条件分支:

( if expr expr expr )

+条件分支一节(本手册后文)中解释了有关 条件分支 的更多详情。

第一个 expr 总是会被求值。如果它产生了非 #f 的值,那么第二个 expr 即求值为整个 if 表达式的结果;否则,第三个 expr 即求值为整个表达式的结果。

例如:
> (if (> 2 3)
      "bigger"
      "smaller")

"smaller"

(define (reply s)
  (if (equal? "hello" (substring s 0 5))
      "hi!"
      "huh?"))

 

> (reply "hello racket")

"hi!"

> (reply "λx:(μα.α→α).xx")

"huh?"

复杂的条件分支可通过嵌条 if 表达式构成。例如,你可以让 reply 函数在收到非字符串时仍然能够工作:

(define (reply s)
  (if (string? s)
      (if (equal? "hello" (substring s 0 5))
          "hi!"
          "huh?")
      "huh?"))

我们不必重复 "huh?" 的情况,因为此函数可写成更好的形式:

(define (reply s)
  (if (if (string? s)
          (equal? "hello" (substring s 0 5))
          #f)
      "hi!"
      "huh?"))

然而这种嵌套的 if 却难以阅读。Racket 通过 andor 的形式提供了更加可读的简写,它们可以配合任意数量的表达式使用:

+组合测试:andor一节(本手册后文)中解释了有关 andor 的更多详情。

( and expr* )
( or expr* )

and 形式遵循短路(short-circuits)求值:当表达式产生了 #f 时,它会停止并返回 #f,否则会继续求值。or 形式在遇到求值为真的结果时同样会短路。

例如:
(define (reply s)
  (if (and (string? s)
           (>= (string-length s) 5)
           (equal? "hello" (substring s 0 5)))
      "hi!"
      "huh?"))
> (reply "hello racket")

"hi!"

> (reply 17)

"huh?"

另一种嵌套 if 的通用模式是调用一系列测试,其中每个测试都有自己的结果:

(define (reply-more s)
  (if (equal? "hello" (substring s 0 5))
      "hi!"
      (if (equal? "goodbye" (substring s 0 7))
          "bye!"
          (if (equal? "?" (substring s (- (string-length s) 1)))
              "I don't know"
              "huh?"))))

这种一系列测试的简写为 cond 形式:

+链式测试:cond一节(本手册后文)中解释了有关 cond 的更多详情。

( cond {[ expr expr* ]}* )

cond 形式在方括号中包含了一系列从句。每个从句的第一个 expr 均为测试表达式。如果它产生真,那么该从句中剩余的 expr 就会被求值, 而该从句中最后一个表达式的结果即为整个 cond 表达式的结果;其余的从句则会被忽略。 如果测试 expr 产生了 #f,那么该从句中剩余的 expr 就会被忽略, 而求值会继续执行下一个从句。最后一个从句可使用 else 作为求值为 #t 的测试表达式的别名。

使用 cond 表达式,reply-more 函数的写法可以更加清晰:

(define (reply-more s)
  (cond
   [(equal? "hello" (substring s 0 5))
    "hi!"]
   [(equal? "goodbye" (substring s 0 7))
    "bye!"]
   [(equal? "?" (substring s (- (string-length s) 1)))
    "I don't know"]
   [else "huh?"]))

 

> (reply-more "hello racket")

"hi!"

> (reply-more "goodbye cruel world")

"bye!"

> (reply-more "what is your favorite color?")

"I don't know"

> (reply-more "mine is lime green")

"huh?"

cond 从句中使用方括号是一种约定。在 Racket 中,圆括号和方括号其实是可以互换的, 只要 () 匹配,[]匹配即可。 在一些关键的地方使用方括号可以让 Racket 代码更加易读。

2.2.6 再谈函数调用

之前我们过度简化了函数调用的语法。函数调用的实际语法允许任意表达式,而不只有 id

+函数调用一节(本手册后文)中解释了有关 函数调用 的更多详情。

( expr expr* )

第一个 expr 通常为 id,例如 string-append+,不过它可以是任何求值为函数的东西。例如,它可以是条件表达式:

(define (double v)
  ((if (string? v) string-append +) v v))

 

> (double "mnah")

"mnahmnah"

> (double 5)

10

单从语法上来说,函数调用中的第一个表达式甚至可以是数字—不过这会产生错误, 因为数字并不是一个函数。

> (1 2 3 4)

application: not a procedure;

 expected a procedure that can be applied to arguments

  given: 1

  arguments...:

   2

   3

   4

如果你不小心忽略了函数名,或者在表达式外使用了额外的括号,那么你通常就会得到这种 “expected a procedure(需要一个过程)”的错误。

2.2.7 匿名函数与 lambda

如果你必须命名所有的数值,那么用 Racket 编程就太过无聊了。 如果你不用 (+ 1 2),那么必须这样写:

+函数:lambda一节(本手册后文)中解释了有关 lambda 的更多详情。

> (define a 1)
> (define b 2)
> (+ a b)

3

事实证明,命名所有的函数同样也很无聊。例如,你有个函数 twice, 它接受另一个函数作为参数。如果作为参数的函数已经有了名字(如 sqrt), 那么使用 twice 会很方便:

(define (twice f v)
  (f (f v)))

 

> (twice sqrt 16)

2

如果你想要调用一个尚未定义的函数,那么必须先定义它才能传入 twice 中:

(define (louder s)
  (string-append s "!"))

 

> (twice louder "hello")

"hello!!"

但如果对 twice 的调用是唯一使用 louder 的地方, 那么完全没必要写下它的完整定义。在 Racket 中,你可以用 lambda 表达式来直接产生一个函数。lambda 形式后面紧跟着函数参数的标识符, 之后是函数体表达式:

( lambda ( id* ) expr+ )

lambda 形式自身进行求值会产生一个函数:

> (lambda (s) (string-append s "!"))

#<procedure>

上面对 twice 的调用可通过 lambda 重写为:

> (twice (lambda (s) (string-append s "!"))
         "hello")

"hello!!"

> (twice (lambda (s) (string-append s "?!"))
         "hello")

"hello?!?!"

Another use of lambda is as a result for a function that generates functions:

lambda 还可用于构造出生成函数作为结果的函数:

(define (make-add-suffix s2)
  (lambda (s) (string-append s s2)))

 

> (twice (make-add-suffix "!") "hello")

"hello!!"

> (twice (make-add-suffix "?!") "hello")

"hello?!?!"

> (twice (make-add-suffix "...") "hello")

"hello......"

Racket 是一个带有词法作用域的语言,这表示 make-add-suffix 返回的函数中的 s2 总是会引述创建该函数的调用的参数。换句话说, 被生成的 lambda 函数“记住了”正确的 s2

> (define louder (make-add-suffix "!"))
> (define less-sure (make-add-suffix "?"))
> (twice less-sure "really")

"really??"

> (twice louder "really")

"really!!"

目前我们只是将形如 (define id expr) 的定义当做“非函数定义”,然而这种刻画方式是具有误导性的。由于 expr 可以是 lambda 的形式,因此在这种情况下,其定义等价于使用“函数”形式的定义。 例如,以下两种 louder 的定义是等价的:

(define (louder s)
  (string-append s "!"))
 
(define louder
  (lambda (s)
    (string-append s "!")))

 

> louder

#<procedure:louder>

请注意在第二种情况下,louder 的表达式为使用 lambda 编写的“匿名函数”。单如果可能的话,编译器总是会推断出一个名字, 以使其打印结果和错误报告的信息尽可能地丰富。

2.2.8 局部绑定与 defineletlet*

现在是时候丰富我们对 Racket 语法的简单认知了。在函数体中,定义可以出现在主体表达式之前:

+内部定义一节(本手册后文)中解释了有关 局部(内部)定义 的更多详情。

( define ( id id* ) definition* expr+ )
( lambda ( id* ) definition* expr+ )

函数体起始处的定义为函数体的局部定义。

例如:
(define (converse s)
  (define (starts? s2) ; 局限于 converse
    (define len2 (string-length s2))  ; 局限于 starts?
    (and (>= (string-length s) len2)
         (equal? s2 (substring s 0 len2))))
  (cond
   [(starts? "hello") "hi!"]
   [(starts? "goodbye") "bye!"]
   [else "huh?"]))
> (converse "hello!")

"hi!"

> (converse "urp")

"huh?"

> starts? ; converse 之外,所以...

starts?: undefined;

 cannot reference an identifier before its definition

  in module: top-level

  internal name: starts?

创建局部绑定的另一种方式是使用 let 形式。let 的一个优点是它可以在表达式中的任何地方使用。此外,let 可一次绑定多个标识符,而无需用 define 分别定义每个标识符。

+内部定义一节(本手册后文)中解释了有关 letlet* 的更多详情。

( let ( {[ id expr ]}* ) expr+ )

每个绑定从句都是用方括号括住的一对 idexpr, 从句之后的表达式则是 let 的主体。在每个从句中,id 均被绑定为 expr 的结果以用在主体中。

> (let ([x (random 4)]
        [o (random 4)])
    (cond
     [(> x o) "X wins"]
     [(> o x) "O wins"]
     [else "cat's game"]))

"X wins"

let 形式中的绑定只能在 let 的主体中使用,因此绑定从句无法互相引述。 而 let* 形式则允许后面的从句使用前面的绑定:

> (let* ([x (random 4)]
         [o (random 4)]
         [diff (number->string (abs (- x o)))])
    (cond
     [(> x o) (string-append "X wins by " diff)]
     [(> o x) (string-append "O wins by " diff)]
     [else "cat's game"]))

"X wins by 1"