2.2 Simple Definitions and Expressions
程序模块写作
#lang ‹langname› ‹topform›*
其中 ‹topform› 是 ‹definition› 或 ‹expr›。REPL 也会对 ‹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› 的结果。
(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"
在底层,函数定义实际上与非函数定义相同,函数名不一定要在函数调用中使用。函数只是另一种值,尽管其打印形式必然不如数字或字符串的打印形式完整。
> piece #<procedure:piece>
> substring #<procedure:substring>
函数定义可以包含多个表达式作为函数主体。在这种情况下,函数被调用时只返回最后一个表达式的值。其他表达式仅为了某些副作用(如打印)而被求值。
(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-append 和 flavor 被求值,但结果从未被使用。函数的结果只是最终表达式 "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-append 和 a+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 语言预定义了许多函数标识符,如 substring 和 string-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› 求值作为结果。
> (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 通过 and 和 or 形式提供了更易读的快捷方式:
Combining Tests: and and or (later in this guide) explains more about and and or.
( and ‹expr›* ) ( or ‹expr›* )
and 形式会短路求值:当某个表达式产生 #f 时它停止并返回 #f,否则继续执行。or 形式类似地在遇到真值时短路求值。
(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?"
请注意,在上面的语法中,and 和 or 形式可以处理任意数量的表达式。
(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 测试表达式的同义词。
使用 cond,reply-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›+ )
函数主体开头的定义对函数主体是局部的。
(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"