On this page:
2.2.1 Definitions
2.2.2 An Aside on Indenting Code
2.2.3 Identifiers
2.2.4 Definitions
2.2.5 Conditionals with if, and, or, and cond
2.2.6 Function Calls, Again
2.2.7 Anonymous Functions with lambda
2.2.8 Local Binding with define, let, and let*

2.2 Simple Definitions and Expressions🔗

程序模块写作

#lang langname topform*

其中 topformdefinitionexprREPL 也会对 topform 求值。

在语法规范中,带灰色背景的文本(如 #lang)表示字面文本。此类字面量与像 id 这样的非终结符之间必须有空白,但 ()[] 前后不需要空白。以 ; 开头直到行末的与空白的处理方式相同。

+(part ("(lib scribblings/reference/reference.scrbl)" "parse-comment")) in (part ("(lib scribblings/reference/reference.scrbl)" "top")) provides more on different forms of comments.

按照通常的约定,语法中的 * 表示前面的元素重复零次或多次,+ 表示重复一次或多次,{} 将序列作为元素分组以便重复。

2.2.1 Definitions🔗

形式为

+Definitions: define (later in this guide) explains more about definitions.

( define id expr )

的定义将 id 绑定到 expr 的结果,而

( define ( id id* ) expr+ )

的定义将第一个 id 绑定到一个函数(也称为过程),该函数接受由其余 id 命名的参数。在函数的情况下,expr 是函数的主体。当函数被调用时,它返回最后一个 expr 的结果。

Examples:
(define pie 3)             ; defines pie to be 3
(define (piece str)        ; defines piece as a function
  (substring str 0 pie))   ;  of one argument
> pie

3

> (piece "key lime")

"key"

在底层,函数定义实际上与非函数定义相同,函数名不一定要在函数调用中使用。函数只是另一种值,尽管其打印形式必然不如数字或字符串的打印形式完整。

Examples:
> piece

#<procedure:piece>

> substring

#<procedure:substring>

函数定义可以包含多个表达式作为函数主体。在这种情况下,函数被调用时只返回最后一个表达式的值。其他表达式仅为了某些副作用(如打印)而被求值。

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

preheating oven...

"apple pie"

Racket 程序员倾向于避免副作用,因此定义通常只有一个表达式作为主体。然而,理解定义主体中允许多个表达式是很重要的,因为它解释了为什么下面的 nobake 函数未能将其参数包含在结果中:

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

 

> (nobake "green")

"jello"

nobake 中,string-append flavor "jello" 周围没有括号,因此它们是三个独立的表达式而不是一个函数调用表达式。表达式 string-appendflavor 被求值,但结果从未被使用。函数的结果只是最终表达式 "jello" 的结果。

2.2.2 An Aside on Indenting Code🔗

换行和缩进对于解析 Racket 程序并不重要,但大多数 Racket 程序员使用一套标准约定来使代码更易读。例如,定义的主体通常缩进在定义的第一行之下。标识符紧跟在左括号后面,没有额外的空格,右括号永远不会单独出现在一行。

当你在程序或 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 Identifiers🔗

Racket 的标识符语法特别宽松。排除特殊字符

+Identifiers and Binding (later in this guide) explains more about identifiers.

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

以及构成数字常量的字符序列外,几乎任何非空白字符序列都构成 id。例如 substring 是一个标识符。同样,string-appenda+b 是标识符,而不是算术表达式。以下是更多示例:

+
integer?
pass/fail
Hfuhruhurr&Uumellmahaye
john-jacob-jingleheimer-schmidt
a-b-c+1-2-3

2.2.4 Definitions🔗

我们已经见过许多函数调用,在更传统的术语中称为过程应用。函数调用的语法是

+Function Calls (later in this guide) explains more about function calls.

( id expr* )

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

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

在文档中的示例 Racket 代码中,预定义名称的使用都链接到参考手册。因此,你可以点击标识符来获取其使用的完整详细信息。

> (string-append "rope" "twine" "yarn")  ; append strings

"ropetwineyarn"

> (substring "corduroys" 0 4)            ; extract a substring

"cord"

> (string-prefix? "shoelace" "shoe")     ; recognize string prefix/suffix

#t

> (string-suffix? "shoelace" "shoe")

#f

> (string? "Ceci n'est pas une string.") ; recognize strings

#t

> (string? 1)

#f

> (sqrt 16)                              ; find a square root

4

> (sqrt -16)

0+4i

> (+ 1 2)                                ; add numbers

3

> (- 2 1)                                ; subtract numbers

1

> (< 2 1)                                ; compare numbers

#f

> (>= 2 1)

#t

> (number? "c'est une number")           ; recognize numbers

#f

> (number? 1)

#t

> (equal? 6 "half dozen")                ; compare anything

#f

> (equal? 6 6)

#t

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

#t

2.2.5 Conditionals with if, and, or, and cond🔗

下一种最简单的表达式是 if 条件表达式:

( if expr expr expr )

+Conditionals (later in this guide) explains more about conditionals.

第一个 expr 总是被求值。如果它产生非 #f 的值,则对第二个 expr 求值作为整个 if 表达式的结果,否则对第三个 expr 求值作为结果。

Example:
> (if (> 2 3)
      "2 is bigger than 3"
      "2 is smaller than 3")

"2 is smaller than 3"

(define (reply s)
  (if (string-prefix? s "hello ")
      "hi!"
      "huh?"))

 

> (reply "hello racket")

"hi!"

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

"huh?"

可以通过嵌套 if 表达式来形成复杂的条件。例如,在前面的 reply 示例中,输入必须是字符串,因为 string-prefix? 在给定非字符串时会报错。你可以通过添加另一个 if 来首先检查输入是否为字符串来消除此限制:

(define (reply-non-string s)
  (if (string? s)
      (if (string-prefix? s "hello ")
          "hi!"
          "huh?")
      "huh?"))

与其重复 "huh?" 的情况,这个函数最好写成

(define (reply-non-string s)
  (if (if (string? s)
          (string-prefix? s "hello ")
          #f)
      "hi!"
      "huh?"))

但这些嵌套的 if 难以阅读。Racket 通过 andor 形式提供了更易读的快捷方式:

+Combining Tests: and and or (later in this guide) explains more about and and or.

( and expr* )
( or expr* )

and 形式会短路求值:当某个表达式产生 #f 时它停止并返回 #f,否则继续执行。or 形式类似地在遇到真值时短路求值。

Examples:
(define (reply-non-string s)
  (if (and (string? s) (string-prefix? s "hello "))
      "hi!"
      "huh?"))
> (reply-non-string "hello racket")

"hi!"

> (reply-non-string 17)

"huh?"

请注意,在上面的语法中,andor 形式可以处理任意数量的表达式。

Examples:
(define (reply-only-enthusiastic s)
  (if (and (string? s)
           (string-prefix? s "hello ")
           (string-suffix? s "!"))
      "hi!"
      "huh?"))
> (reply-only-enthusiastic "hello racket!")

"hi!"

> (reply-only-enthusiastic "hello racket")

"huh?"

另一种常见的嵌套 if 模式涉及一系列测试,每个测试有自己的结果:

(define (reply-more s)
  (if (string-prefix? s "hello ")
      "hi!"
      (if (string-prefix? s "goodbye ")
          "bye!"
          (if (string-suffix? s "?")
              "I don't know"
              "huh?"))))

一系列测试的简写是 cond 形式:

+Chaining Tests: cond (later in this guide) explains more about cond.

( cond {[ expr expr* ]}* )

cond 形式在方括号之间包含一系列子句。在每个子句中,第一个 expr 是测试表达式。如果它产生真值,则对子句的其余 expr 求值,子句中的最后一个为整个 cond 表达式提供答案;其余子句被忽略。如果测试 expr 产生 #f,则子句的其余 expr 被忽略,继续对下一个子句求值。最后一个子句可以使用 else 作为 #t 测试表达式的同义词。

使用 condreply-more 函数可以更清晰地写成:

(define (reply-more s)
  (cond
   [(string-prefix? s "hello ")
    "hi!"]
   [(string-prefix? s "goodbye ")
    "bye!"]
   [(string-suffix? s "?")
    "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 Function Calls, Again🔗

在我们之前关于函数调用的语法中,我们做了过度简化。函数调用的实际语法允许函数使用任意表达式,而不仅仅是 id

+Function Calls (later in this guide) explains more about function calls.

( 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

当你意外省略函数名或在表达式周围使用额外的括号时,你最常会得到像这样的"expected a procedure"错误。

2.2.7 Anonymous Functions with lambda🔗

如果必须给所有数字命名,在 Racket 中编程会很乏味。与其写 (+ 1 2),你不得不写

+Functions: lambda (later in this guide) explains more about 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>

使用 lambda,上面的 twice 调用可以重写为

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

"hello!!"

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

"hello?!?!"

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 Local Binding with define, let, and let*🔗

是时候撤回我们在 Racket 语法中的另一个简化了。在函数主体中,定义可以出现在主体表达式之前:

+Internal Definitions (later in this guide) explains more about local (internal) definitions.

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

函数主体开头的定义对函数主体是局部的。

Examples:
(define (converse s)
  (define (starts? s2) ; local to converse
    (define spaced-s2 (string-append s2 " ")) ; local to starts?
    (string-prefix? s spaced-s2))
  (cond
    [(starts? "hello") "hi!"]
    [(starts? "goodbye") "bye!"]
    [else "huh?"]))
> (converse "hello world")

"hi!"

> (converse "hellonearth")

"huh?"

> (converse "goodbye friends")

"bye!"

> (converse "urp")

"huh?"

> starts? ; outside of converse, so...

starts?: undefined;

 cannot reference an identifier before its definition

  in module: top-level

创建局部绑定的另一种方式是 let 形式。let 的一个优点是它可以在任何表达式位置使用。此外,let 可以同时绑定多个标识符,而不需要为每个标识符单独写一个 define

+Internal Definitions (later in this guide) explains more about let and let*.

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

每个绑定子句是一个 id 和一个用方括号包围的 expr,子句之后的表达式是 let 的主体。在每个子句中,id 绑定到 expr 的结果以供主体使用。

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

"cat's game"

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"