Racket编程指南——2 Racket概要

2 Racket概要

本章提供了一个对Racket的快速入门作为给这个指南余下部分的背景。有一些Racket经验的读者可以直接跳到《内置的数据类型》部分。

    2.1 简单的值

    2.2 简单的定义和表达式

      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.3 列表、迭代和递归

      2.3.1 预定义列表循环

      2.3.2 从头开始列表迭代

      2.3.3 尾递归

      2.3.4 递归和迭代

    2.4 pair、list和Racket的语法

      2.4.1 quote引用pair和symbol

      2.4.2 使用'缩写quote

      2.4.3 列表和Racket语法

2.1 简单的值

Racket值包括数值、布尔值、字符串和字节字符串。在DrRacket和文档示例中(当你在着色状态下阅读文档时),值表达式显示为绿色。

数值(number)以通常的方式书写,包括分数和虚数:

数值(Number) (later in this guide) explains more about 数值(Numbers).

1       3.14
1/2     6.02e+23
1+2i    9999999999999999999999

布尔值(boolean)#t表示真,用#f表示假。然而,在条件从句中,所有非#f值被视为真。

布尔值(Boolean) (later in this guide) explains more about 布尔值(boolean).

字符串(string)写在双引号("")之间。在一个字符串中,反斜杠(/)是一个转义字符;例如,一个反斜杠之后的一个双引号包括了字符串中的一个字面上的双引号。除了一个保留的双引号或反斜杠,任何Unicode字符都可以在字符串常量中出现。

字符串(Unicode) (later in this guide) explains more about 字符串(string).

"Hello, world!"
"Benjamin \"Bugsy\" Siegel"
"λx:(μα.α→α).xx"

当一个常量在REPL中被求值时,它通常打印与输入语法相同的结果。在某些情况下,打印格式是输入语法的一个标准化版本。在文档和在DrRacket的REPL中,结果打印为蓝色而不是绿色以突出打印结果与输入表达式之间的区别。

Examples:
> 1.0000

1.0

> "Bugs \u0022Figaro\u0022 Bunny"

"Bugs \"Figaro\" Bunny"

2.2 简单的定义和表达式

一个程序模块一般被写作

#lang langname topform*

topform既是一个definition也是一个exprREPL也对topform求值。

在语法规范里,文本使用灰色背景,比如#lang,代表文本。除了()[]之前或之后不需要空格之外,文本与非结束符(像ID)之间必须有空格。注释以;开始,直至这一行结束,空白也做相同处理。

《Racket参考》中的“(parse-comment)”提供有更多的有关注释的不同形式内容。

以后的内容遵从如下惯例:*在程序中表示零个或多个前面元素的重复,+表示一个或多个前面元素的重复,{} 组合一个序列作为一个元素的重复。

2.2.1 定义

表的一个定义:

定义:define (later in this guide) explains more about 定义.

( define id expr )

绑定idexpr的结果,而

( define ( id id* ) expr+ )

绑定第一个id到一个函数(也叫一个程序),它通过余下的id以参数作为命名。在函数情况下,该expr是函数的函数体。当函数被调用时,它返回最后一个expr的结果。

Examples:
(define pie 3)             ; 定义pie3
 
(define (piece str)        ; 定义piece为一个
  (substring str 0 pie))   ; 带一个参数的函数
 
> pie

3

> (piece "key lime")

"key"

在底层,一个函数定义实际上与一个非函数定义相同,并且一个函数名不是不需在一个函数调用中使用。一个函数只是另一种类型的值,尽管打印形式不一定比数字或字符串的打印形式更完整。

Examples:
> piece

#

> substring

#

一个函数定义能够包含函数体的多个表达式。在这种情况下,在调用函数时只返回最后一个表达式的值。其它表达式只对一些副作用进行求值,比如打印。

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

pre-heating 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 缩进代码的提示

换行和缩进对于解析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 标识符

Racket对标识符的语法是特别自由的。但排除以下特殊字符。

标识符和绑定 (later in this guide) explains more about 标识符.

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

同时除了产生数字常数的字符序列,几乎任何非空白字符序列形成一个id。例如substring是一个标识符。另外,string-appenda+b是标识符,而不是算术表达式。这里还有几个更多的例子:

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

我们已经看到过许多函数调用,更传统的术语称之为过程应用程序。函数调用的语法是:

函数调用 (later in this guide) explains more about 函数调用.

( 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 )

条件 (later in this guide) explains more about 条件句.

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

Example:
> (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表达式构成。例如,当给定非字符串(non-strings)时,你可以编写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 (later in this guide) explains more about and and or.

( and expr* )
( or expr* )

and表绕过情况:当一个表达式产生#f,它停止并返回#f,否则它继续运行。当or表遇到一个真的结果时,它同样的产生绕过情况。

Examples:
(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 (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
   [(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

函数调用 (later in this guide) explains more about 函数调用.

( 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函数(过程) (later in this guide) explains more about lambda.

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

3

事实证明,要命名所有你的函数也可能是很乏味的。例如,你可能有一个函数 twice,它带了一个函数和一个参数。如果你已经有了这个函数的名字,那么使用 twice是比较方便的,如sqrt

(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 "!"))

#

使用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是一个词法作用域(lexically scoped)语言,这意味着函数中的s2总是通过make-add-suffix引用创建该函数调用的参数返回。换句话说,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)的定义的一定程度的引用作为“非函数定义(non-function definitions)“。这种表征是误导性的,因为expr可以是一个lambda表,在这种情况下,定义与使用“函数(function)”定义表是等价的。例如,下面两个louder的定义是等价的:

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

 

> louder

#

注意,对第二例子中louder的表达式是用lambda写成的“匿名”函数,但如果可能的话,无论如何,编译器推断出一个名称以使打印和错误报告尽可能地提供信息。

2.2.8 defineletlet*实现局部绑定

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

内部定义 (later in this guide) explains more about 局部(内部)定义.

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

在一个函数主体的开始的定义对这个函数主体来说是局部的。

Examples:
(define (converse s)
  (define (starts? s2) ; local to converse
    (define len2 (string-length s2))  ; local to 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? ; outside of converse, so...

starts?: undefined;

 cannot reference undefined identifier

创建局部绑定的另一种方法是let表。let的一个优势是它可以在任何表达式位置使用。另外,let可以一次绑定多个标识符,而不是每个标识符都需要一个单独的define

内部定义 (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"]))

"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 3"

2.3 列表、迭代和递归

Racket语言是Lisp语言的一种方言,名字来自于“LISt Processor”。内置的列表数据类型保留了这种语言的一个显著特征。

list函数接受任意数量的值并返回一个包含这些值的列表:

> (list "red" "green" "blue")

'("red" "green" "blue")

> (list 1 2 3 4 5)

'(1 2 3 4 5)

一个列表通常用'打印,但是一个列表的打印形式取决于它的内容。更多信息请看《点对(Pair)和列表(List)》。

就如你能够看到的那样,一个列表结果在REPL中打印为一个引用',并且采用一对圆括号包围这个列表元素的打印表。这里有一个容易混淆的地方,因为两个表达式都使用圆括号,比如(list "red" "green" "blue"),那么打印结果为'("red""green" "blue")。除了引用,结果的圆括号在文档中和在DrRacket中打印为蓝色,而表达式的圆括号是棕色的。

在列表方面有许多预定义的函数操作。下面是少许例子:

> (length (list "hop" "skip" "jump"))        ; count the elements

3

> (list-ref (list "hop" "skip" "jump") 0)    ; extract by position

"hop"

> (list-ref (list "hop" "skip" "jump") 1)

"skip"

> (append (list "hop" "skip") (list "jump")) ; combine lists

'("hop" "skip" "jump")

> (reverse (list "hop" "skip" "jump"))       ; reverse order

'("jump" "skip" "hop")

> (member "fall" (list "hop" "skip" "jump")) ; check for an element

#f

2.3.1 预定义列表循环

除了像append这样的简单操作,Racket还包括遍历列表元素的函数。这些迭代函数扮演类似于java、Racket及其它语言里的for一个角色。一个Racket迭代的主体被打包成一个应用于每个元素的函数,所以lambda表在与迭代函数的组合中变得特别方便。

不同的列表迭代函数在不同的方式中组合迭代结果。map函数使用每个元素结果创建一个新列表:

> (map sqrt (list 1 4 9 16))

'(1 2 3 4)

> (map (lambda (i)
         (string-append i "!"))
       (list "peanuts" "popcorn" "crackerjack"))

'("peanuts!" "popcorn!" "crackerjack!")

andmapormap函数通过andor结合结果:

> (andmap string? (list "a" "b" "c"))

#t

> (andmap string? (list "a" "b" 6))

#f

> (ormap number? (list "a" "b" 6))

#t

mapandmapormap函数都可以处理多个列表,而不只是一个单一的列表。这些列表都必须具有相同的长度,并且给定的函数必须对每个列表接受一个参数:

> (map (lambda (s n) (substring s 0 n))
       (list "peanuts" "popcorn" "crackerjack")
       (list 6 3 7))

'("peanut" "pop" "cracker")

filter函数保持其主体结果是真的元素,并忽略其主体结果是#f的元素:

> (filter string? (list "a" "b" 6))

'("a" "b")

> (filter positive? (list 1 -2 6 7 0))

'(1 6 7)

foldl函数包含某些迭代函数。它使用每个元素函数处理一个元素并将其与“当前”值相结合,因此每个元素函数接受额外的第一个参数。另外,在列表之前必须提供一个开始的“当前”值:

> (foldl (lambda (elem v)
           (+ v (* elem elem)))
         0
         '(1 2 3))

14

尽管有其共性,foldl不是像其它函数一样受欢迎。一个原因是map ormapandmapfilter覆盖了最常见的列表迭代。

Racket提供了一个通用的列表理解for/list,它用迭代通过序列(sequences)来建立一个列表。列表理解表和相关迭代表将在《迭代和推导》部分描述。

2.3.2 从头开始列表迭代

尽管map和其它迭代函数是预定义的,但它们在任何令人感兴趣的意义上都不是原始的。使用少量列表原语即能编写等效迭代。

由于一个Racket列表是一个链表,对非空列表的两个核心操作是:

  • first:取得列表上的第一个元素;

  • rest:获取列表的其余部分。

Examples:
> (first (list 1 2 3))

1

> (rest (list 1 2 3))

'(2 3)

为一个链表添加一个新的节点——确切地说,添加到这个列表的前面——使用cons函数,它是“construct”(构造)的缩写。要得到一个空列表用于开始,用empty来构造:

> empty

'()

> (cons "head" empty)

'("head")

> (cons "dead" (cons "head" empty))

'("dead" "head")

要处理一个列表,你需要能够区分空列表和非空列表,因为firstrest只在非空列表上工作。empty?函数检测空列表,cons?检测非空列表:

> (empty? empty)

#t

> (empty? (cons "head" empty))

#f

> (cons? empty)

#f

> (cons? (cons "head" empty))

#t

通过这些片段,你能够编写你自己的length函数、map函数以及更多的函数的版本。

Examples:
(define (my-length lst)
  (cond
   [(empty? lst) 0]
   [else (+ 1 (my-length (rest lst)))]))
 
> (my-length empty)

0

> (my-length (list "a" "b" "c"))

3

(define (my-map f lst)
  (cond
   [(empty? lst) empty]
   [else (cons (f (first lst))
               (my-map f (rest lst)))]))

 

> (my-map string-upcase (list "ready" "set" "go"))

'("READY" "SET" "GO")

如果上述定义的派生对你来说难以理解,建议去读《How to Design Programs(如何设计程序)》。如果你只对使用递归调用表示不信任,而不是循环结构,那就继续往后读。

2.3.3 尾递归

my-length函数和my-map函数都为一个 n长度的列表在O(n)空间内运行。通过想象就很容易明白必须怎样(my-length (list "a" "b" "c"))求值:

(my-length (list "a" "b" "c"))
= (+ 1 (my-length (list "b" "c")))
= (+ 1 (+ 1 (my-length (list "c"))))
= (+ 1 (+ 1 (+ 1 (my-length (list)))))
= (+ 1 (+ 1 (+ 1 0)))
= (+ 1 (+ 1 1))
= (+ 1 2)
= 3

对于一个带有n个元素的列表,求值将堆栈叠加n(+ 1 ...),并且直到列表用完时最后才把它们加起来。

你可以通过一路求和避免堆积添加。要以这种方式累积一个长度,我们需要一个既可以操作列表也可以操作当前列表长度的函数;下面的代码使用一个局部函数iter,它在一个参数len中累积长度:

(define (my-length lst)
  ; 局部函数iter:
  (define (iter lst len)
    (cond
     [(empty? lst) len]
     [else (iter (rest lst) (+ len 1))]))
  ; my-length的主体调用iter:
  (iter lst 0))

现在求值过程看起来像这样:

(my-length (list "a" "b" "c"))
= (iter (list "a" "b" "c") 0)
= (iter (list "b" "c") 1)
= (iter (list "c") 2)
= (iter (list) 3)
3

修正后的my-length在恒定的空间中运行,正如上面所建议的求值步骤那样。也就是说,当一个函数调用的结果,像(iter (list "b" "c") 1),恰恰是其它函数调用的结果,像(iter (list "c")),那么第一个函数不需要等待第二个函数回绕,因为那样会为了不恰当的原因占用空间。

这种求值行为有时称为”尾部调用优化(tail-call optimization)“,但它在Racket里不仅仅是一种“优化”,它是一种代码将要运行的方式的保证。更确切地说,相对于另一表达式的一个尾部(tail position)位置的表达式在另一表达式上不占用额外的计算空间。

my-map例子中,O(n)空间复杂度是合理的,因为它必须生成一个O(n)大小的结果。不过,你可以通过累积结果列表来减少常量因子。唯一的问题是累积的列表将是向后的,所以你将不得不在每个结尾处反转它:

如下面所述,试图减少像这样的一个常量因子通常是不值得的。

(define (my-map f lst)
  (define (iter lst backward-result)
    (cond
     [(empty? lst) (reverse backward-result)]
     [else (iter (rest lst)
                 (cons (f (first lst))
                       backward-result))]))
  (iter lst empty))

事实证明,如果你这样写:

(define (my-map f lst)
  (for/list ([i lst])
    (f i)))

那么函数中的for/list表扩展到和iter函数局部定义和使用在本质上相同的代码。区别仅仅是句法上的便利。

2.3.4 递归和迭代

my-lengthmy-map示例表明迭代只是递归的一个特例。在许多语言中,尽可能地将尽可能多的计算合并成迭代形式是很重要的。否则,性能会变差,不太大的输入都会导致堆栈溢出。类似地,在Racket中,有时很重要的一点是要确保在易于计算的常数空间中使用尾递归避免O(n)空间消耗。

然而,在Racket里递归不会导致特别差的性能,而且没有堆栈溢出那样的事情;如果一个计算涉及到太多的上下文,你可能耗尽内存,但耗尽内存通常需要比可能触发其它语言中的堆栈溢出更多数量级以上的更深层次的递归。基于这些考虑因素,加上尾递归程序会自动和一个循环一样运行的事实相结合,引导Racket程序员接受递归形式而不是避免它们。

例如,假设你想从一个列表中去除连续的重复项。虽然这样的一个函数可以写成一个循环,为每次迭代记住前面的元素,但一个Racket程序员更可能只写以下内容:

(define (remove-dups l)
  (cond
   [(empty? l) empty]
   [(empty? (rest l)) l]
   [else
    (let ([i (first l)])
      (if (equal? i (first (rest l)))
          (remove-dups (rest l))
          (cons i (remove-dups (rest l)))))]))

 

> (remove-dups (list "a" "b" "b" "b" "c" "c"))

'("a" "b" "c")

一般来说,这个函数为一个长度为n的输入列表消耗O(n)的空间,但这很好,因为它产生一个O(n)结果。如果输入列表恰巧是连续重复的,那么得到的列表可以比O(n)小得多——而且remove-dups也将使用比O(n)更少的空间!原因是当函数放弃重复,它返回一个remove-dups的直接调用结果,所以尾部调用“优化”加入:

(remove-dups (list "a" "b" "b" "b" "b" "b"))
= (cons "a" (remove-dups (list "b" "b" "b" "b" "b")))
= (cons "a" (remove-dups (list "b" "b" "b" "b")))
= (cons "a" (remove-dups (list "b" "b" "b")))
= (cons "a" (remove-dups (list "b" "b")))
= (cons "a" (remove-dups (list "b")))
= (cons "a" (list "b"))
= (list "a" "b")

2.4 pair、list和Racket的语法

cons函数实际上接受任意两个值,而不只是一个给第二个参数的列表。当第二个参数不是empty且不是自己通过cons产生的时,结果以一种特殊的方式打印出来。两个值用cons凑在一起被打印在括号之间,但在两者之间有一个点(即,一个被空格环绕的句点):

> (cons 1 2)

'(1 . 2)

> (cons "banana" "split")

'("banana" . "split")

因此,由cons产生的一个值并不总是一个列表。一般来说,cons的结果是一个 点对(pair)。更符合惯例的cons?函数名字是pair?,那我们从现在开始使用这个符合惯例的名字。

名字rest对非列表点对也意义不大;对firstrest更符合惯例的名字分别是carcdr。(当然,符合惯例的名字也是没有意义的。请记住,“a”出现在“d”之前,并且cdr被声明为“could-er(可以)”。

Examples:
> (car (cons 1 2))

1

> (cdr (cons 1 2))

2

> (pair? empty)

#f

> (pair? (cons 1 2))

#t

> (pair? (list 1 2 3))

#t

Racket的点对数据类型和它对表的关系,连同打印的点符号和滑稽的名字carcdr本质上是一个历史上的奇特事物。然而,点对深深地被连接进了Racket的文化、详述和实现上,因此它们在语言中得以存在下来。

在你犯一个错误时,你很可能会遇到一个非列表点对,比如不小心给cons把参数颠倒过来:

> (cons (list 2 3) 1)

'((2 3) . 1)

> (cons 1 (list 2 3))

'(1 2 3)

非列表点对有时被有意使用。例如,make-hash函数取得了一个点对的列表,其中每个点对的car是一个键同时cdr是一个任意值。

对新的Racket程序员唯一更困惑的情况莫过于非列表点对是对点对的打印习惯,其第二个元素一个点对而不是一个列表:

> (cons 0 (cons 1 2))

'(0 1 . 2)

一般来说,打印一个点对的规则如下:除非该点紧接着是一个开括号,否则使用点表示法。在这种情况下,去掉点、开括号和匹配的闭括号。由此,'(0 . (1 . 2))变成'(0 1 . 2)'(1 . (2 . (3 . ())))变成'(1 2 3)

2.4.1 quote引用pair和symbol

一个列表在前面打印一个引号标记,但是如果一个列表的一个元素本身是一个列表,那么就不会为内部列表打印引号标记:

> (list (list 1) (list 2 3) (list 4))

'((1) (2 3) (4))

对于嵌套列表,尤其是quote表,你可以将列表作为一个表达式来写,基本上与列表打印的方式相同:

> (quote ("red" "green" "blue"))

'("red" "green" "blue")

> (quote ((1) (2 3) (4)))

'((1) (2 3) (4))

> (quote ())

'()

无论引用表是否由点括号消除规则规范,quote表都要包含点符号:

> (quote (1 . 2))

'(1 . 2)

> (quote (0 . (1 . 2)))

'(0 1 . 2)

当然,任何种类的列表都可以嵌套:

> (list (list 1 2 3) 5 (list "a" "b" "c"))

'((1 2 3) 5 ("a" "b" "c"))

> (quote ((1 2 3) 5 ("a" "b" "c")))

'((1 2 3) 5 ("a" "b" "c"))

如果用quote包裹标识符,则得到看起来像一个标识符的输出,但带有一个'前缀:

> (quote jane-doe)

'jane-doe

像一个引用标识符那样打印的一个值是一个symbol(符号)。同样,括号输出不应该和表达式混淆,一个打印符号不应与一个标识符混淆。特别是,除了符号和标识符碰巧由相同的字母组成外,符号(quote map)map标识符或绑定到map的预定义函数无关。

的确,一个符号固有的值不过是它的字符内容。从这个意义上说,符号和字符串几乎是一样的东西,主要区别在于它们是如何打印的。函数symbol->stringstring->symbol在它们之间转换。

Examples:
> map

#

> (quote map)

'map

> (symbol? (quote map))

#t

> (symbol? map)

#f

> (procedure? map)

#t

> (string->symbol "map")

'map

> (symbol->string (quote map))

"map"

同样,对一个列表quote会自己自动作用于嵌套列表,在标识符的一个括号序列上的quote会自己自动应用到标识符上以创建一个符号列表:

> (car (quote (road map)))

'road

> (symbol? (car (quote (road map))))

#t

当一个符号在一个打印有'的列表中时,在符号上的这个'被省略了,因为'已经在做这项工作了:

> (quote (road map))

'(road map)

quote表对一个文字表达式,像一个数字或一个字符串这样的,没有影响:

> (quote 42)

42

> (quote "on the record")

"on the record"

2.4.2 使用'缩写quote

你可能已经猜到了,你可以通过仅放置一个'在一个表前面来缩写一个quote的使用:

> '(1 2 3)

'(1 2 3)

> 'road

'road

> '((1 2 3) road ("a" "b" "c"))

'((1 2 3) road ("a" "b" "c"))

在文档中,在一个表达式中的'和后面的表单一起被打印成绿色,因为这个组合是一个表达式,它是一个常量。在DrRacket,只有'被渲染成绿色。DrRacket更加精确校正,因为quote的意义可以根据一个表达式的上下文而变化。然而,在文档中,我们经常假定标准绑定是在范围内的,因此我们为了更清晰用绿色绘制引用表。

一个'以字面相当的方式扩展成一个quote表。你够明白如果你在一个有一个'的表前面放置一个'的这种情况:

> (car ''road)

'quote

> (car '(quote road))

'quote

'缩写在输出和输入中起作用。在打印输出时,REPL打印机识别符号'quote的两元素列表的第一个元素,在这种情况下,它使用打印输出:

> (quote (quote road))

''road

> '(quote road)

''road

> ''road

''road

2.4.3 列表和Racket语法

现在你已经知道了关于点对和列表的真相,而且现在你已经明白了quote,你已经准备好理解我们一直在简化Racket真实语法的主要方法。

Racket的语法并不是直接在字符流中定义的。相反,语法是由两个层确定的:

  • 一个读取器(reader)层,将字符序列转换成列表、符号和其它常量;

  • 一个扩展器(expander)层,它处理列表、符号和其它常量,并将它们解析为表达式。

打印和读取的规则是互相协调的。例如,一个列表用圆括号打印,读取一对圆括号生成一个列表。类似地,一个非列表点对用点表示法打印,同时在输入上的一个点有效地运行点标记规则从反向得到一个点对。

读取层给表达式的一个推论是你可以在不被引用的表的表达式中使用点标记:

> (+ 1 . (2))

3

这个操作因为(+ 1 . (2))只是编写(+ 1 2)的另一种方法。用这种点表示法编写应用程序表达式实际上从来不是一个好主意,它只是Racket语法定义方法的一个推论。

通常,.被仅只带一个括号序列的读取器允许,并且只有在序列的最后一个元素之前。然而,一对.也可以出现在一个括号序列的一个单个元素周围,只要这个元素不是第一个或最后一个。这样的一个点对触发一个阅读器转换,它将.之间的元素移动到列表的前面。这个转换使一种通用的中缀表示法成为可能:

> (1 . < . 2)

#t

> '(1 . < . 2)

'(< 1 2)

这两个点转换是非传统的,并且它与非列表点对的点记法基本上没有关系。Racket程序员保守地使用中缀标记——大多用于非对称二元操作符,如<is-a?

你可能感兴趣的:(Lisp,Racket编程指南(中文译),Racket)