天天看點

Racket程式設計指南——4 表達式和定義

4 表達式和定義

《Racket概要》這一章介紹了一些基本的Racket的句法表:定義、過程程式、條件表達式等等。本節提供這些形式的更詳細資訊,以及一些附加的基本表。

    4.1 标記法
    4.2 辨別符和綁定
    4.3 函數調用(過程程式)
      4.3.1 求值順序和實參數量
      4.3.2 關鍵字參數
      4.3.3 apply函數
    4.4 lambda函數(過程)
      4.4.1 申明一個剩餘(rest)參數
      4.4.2 聲明可選(optional)參數
      4.4.3 聲明關鍵字(keyword)參數
      4.4.4 實參數量感覺函數:case-lambda
    4.5 定義:define
      4.5.1 函數簡寫
      4.5.2 柯裡函數簡寫
      4.5.3 多值和define-values
      4.5.4 内部定義
    4.6 局部綁定
      4.6.1 并行綁定:let
      4.6.2 順序綁定:let*
      4.6.3 遞歸綁定:letrec
      4.6.4 命名let
      4.6.5 多值綁定:let-values,let*-values,letrec-values
    4.7 條件
      4.7.1 簡單分支:if
      4.7.2 組合測試:and和or
      4.7.3 編鍊測試:cond
    4.8 定序
      4.8.1 前效應:begin
      4.8.2 後效應:begin0
      4.8.3 if效應:when和unless
    4.9 指派:set!
      4.9.1 使用指派的指導原則
      4.9.2 多值指派:set!-values
    4.10 引用:quote和'
    4.11 準引用:quasiquote和‘
    4.12 簡單分派:case
    4.13 動态綁定:parameterize

4.1 标記法

這一章(以及其餘的文檔)使用了一個稍微不同的标記法,而不是基于字元的《Racket概要》章裡的文法。對于一個句法表something的使用表現為如下方式:

(something [id ...+] an-expr ...)

在本規範中斜體的元變量,如id和an-expr,使用Racket辨別符的文法,是以an-expr是一進制變量。一個命名約定隐式地定義了許多元變量的含義:

  • 一個以id結束的元變量代表一個辨別符,如x或my-favorite-martian。
  • 一個以keyword結束的元辨別符代表一個關鍵字,如#:tag。
  • 一個以expr結束的元辨別符代表任意子表,它将被解析為一個表達式。
  • 一個以body結束的元辨別符代表任意子表;它将被解析為一個局部定義或者一個表達式。一個body隻有不被任何表達式前置時才能解析為一個定義,并且最後一個body必須是一個表達式;參見《内部定義》部分。

在文法中的方括号表示表的一個括号序列,這裡方括号通常被使用(約定)。也就是說,方括号并不表示是句法表的可選部分。

一個...表示前置表的零個或多個重複,...+表示前置資料的一個或多個重複。另外,非斜體辨別符代表它們自己。

那麼,基于上面的文法,這裡有一些something的與以上相符合的用法:

(something [x])
(something [x] (+ 1 2))
(something [x my-favorite-martian x] (+ 1 2) #f)

一些文法表規範指既不是隐式定義的也不是預定義的元變量。這樣的元變量在主表後面定義,使用一個BNF-like表提供選擇:

(something-else [thing ...+] an-expr ...)
thing = thing-id
| thing-keyword

上面的例子表明,在一個something-else表中,一個thing要麼是一個辨別符要麼是一個關鍵字。

4.2 辨別符和綁定

一個表達式的上下文決定表達式中出現的辨別符的含義。特别是,用語言racket開始一個子產品時,如:

#lang racket

意味着,在子產品中,辨別符在本指南中的描述開始于這裡意義的描述:cons引用建立了一個配對的函數,car引用提取了一個配對的第一個元素的函數,等等。

Racket程式設計指南——4 表達式和定義
《符号(Symbol)》介紹了辨別符文法。

諸如define、lambda和let之類的表,用一個或多個辨別符關聯一個意義;也就是說,它們綁定(bind)辨別符。綁定應用的程式部分是綁定的範圍(scope)。對一個給定的表達式有效的綁定集是表達式的環境(environment)。

例如,有以下内容:

#lang racket
(define f
  (lambda (x)
    (let ([y 5])
      (+ x y))))
(f 10)

define是f的綁定,lambda有一個對x的綁定,let有一個對y的綁定,對f的綁定範圍是整個子產品;x綁定的範圍是(let ([y 5]) (+ x y));y綁定的範圍僅僅是(+ xy)的環境包括對y、x和f的綁定,以及所有在racket中的綁定。

一個子產品級的define僅能夠綁定沒有被定義過或者require進子產品的辨別符。然而,一個局部define或其它綁定表,能夠給一個已經有一個綁定的标志符以一個新的局部綁定;這樣的一個綁定覆寫(shadows)已經存在的綁定。

Examples:

(define f
  (lambda (append)
    (define cons (append "ugly" "confusing"))
    (let ([append 'this-was])
      (list append cons))))
> (f list)
'(this-was ("ugly" "confusing"))

類似地,一個子產品級define可以從這個子產品的語言覆寫一個綁定。例如,一個racket子產品裡的(define cons 1)覆寫被racket提供的cons。故意覆寫一個語言綁定絕對是一個好主意——尤其對于像cons這種被廣泛使用的綁定——但是覆寫把一個程式員從不得不去避免每一個晦澀的通過一個語言提供的綁定中解脫出來。

即使像define和lambda這些從綁定中得到它們的意義,盡管它們有轉換器(transformer)綁定(這意味着它們表明文法表)而不是值綁定。由于define有一個轉換器綁定,這個辨別符define不能被它自己使用于擷取一個值。然而,對define的正常綁定可以被覆寫。

Examples:

> define
eval:1:0: define: bad syntax
  in: define
> (let ([define 5]) define)
5

同樣,用這種方式來覆寫标準綁定絕對是一個好主意,但這種可能性是Racket的靈活性一個固有部分。

4.3 函數調用(過程程式)

表的一個表達式:

(proc-expr arg-expr ...)

是一個函數調用——也被稱為一個應用程式(procedure application)——當proc-expr不是一個被綁定為一個文法翻譯器(如if或define)的辨別符時。

4.3.1 求值順序和實參數量

一個函數調用通過首先求值proc-expr并都按順序(由左至右)來求值。然後,如果arg-expr産生一個接受arg-expr提供的所有參數的函數,這個函數被調用。否則,将引發一個異常。

Examples:

> (cons 1 null)
'(1)
> (+ 1 2 3)
6
> (cons 1 2 3)
cons: arity mismatch;
 the expected number of arguments does not match the given
number
  expected: 2
  given: 3
  arguments...:
   1
   2
   3
> (1 2 3)
application: not a procedure;
 expected a procedure that can be applied to arguments
  given: 1
  arguments...:
   2
   3

某些函數,如cons,接受一個固定數量的參數。某些函數,如+或list,接受任意數量的參數。一些函數接受一系列參數計數;例如substring既接受兩個參數也接受三個參數。一個函數的實參數量(arity)是它接受參數的數量。

4.3.2 關鍵字參數

除了通過位置參數外,有些函數接受關鍵字參數(keyword arguments)。是以,一個arg可以是一個arg-keyword arg-expr序列而不僅僅隻是一個arg-expr:

Racket程式設計指南——4 表達式和定義
《關鍵字(Keyword)》介紹了關鍵字。
(proc-expr arg ...)
arg = arg-expr
| arg-keyword arg-expr

例如:

(go "super.rkt" #:mode 'fast)

用"super.rkt"作為一個位置參數調用這個函數綁定到go,并用'fast作為一個參數與#:mode關鍵字關聯。一個關鍵字隐式地與它後面的表達式配對。

既然一個關鍵字本身不是一個表達式,那麼

(go "super.rkt" #:mode #:fast)

就是一個文法錯誤。#:mode關鍵字必須跟着一個表達式以産生一個參數值,并且#:fast不是一個表達式。

關鍵字arg的順序決定arg-expr求值的順序,而一個函數接受關鍵字參數不依賴于參數清單中的位置。上面對go的調用可以等價地編寫為:

(go #:mode 'fast "super.rkt")
在《Racket參考》的“(application)”部分提供了有關過程程式的更多資訊。
4.3.3 apply函數

函數調用的文法支援任意數量的參數,但是一個特定的調用總是指定一個固定數量的參數。是以,一個帶一個參數清單的函數不能直接應用一個類似于+的函數到一個清單的所有項中:

(define (avg lst) ; 不會運作……
  (/ (+ lst) (length lst)))
> (avg '(1 2 3))
+: contract violation
  expected: number?
  given: '(1 2 3)
(define (avg lst) ; 不總會運作……
  (/ (+ (list-ref lst 0) (list-ref lst 1) (list-ref lst 2))
     (length lst)))
> (avg '(1 2 3))
2
> (avg '(1 2))
list-ref: index too large for list
  index: 2
  in: '(1 2)

apply函數提供了一種繞過這種限制的方法。它使用一個函數和一個list參數,并将函數應用到清單中的值:

(define (avg lst)
  (/ (apply + lst) (length lst)))
> (avg '(1 2 3))
2
> (avg '(1 2))
3/2
> (avg '(1 2 3 4))
5/2

為友善起見,apply函數接受函數和清單之間的附加參數。額外的參數被有效地cons到參數清單:

(define (anti-sum lst)
  (apply - 0 lst))
> (anti-sum '(1 2 3))
-6

apply函數也接受關鍵字參數,并将其傳遞給調用函數:

(apply go #:mode 'fast '("super.rkt"))
(apply go '("super.rkt") #:mode 'fast)

包含在apply的清單參數中的關鍵字不算作調用函數的關鍵字參數;相反,這個清單中的所有參數都被作為位置參數對待。要将一個關鍵字參數清單傳遞給一個函數,使用keyword-apply函數,它接受一個要應用的函數和三個清單。前兩個清單是平行的,其中第一個清單包含關鍵字(按keyword<?排序),第二個清單包含一個與每個關鍵字對應的參數。第三個清單包含位置函數參數,就像apply。

(keyword-apply go
               '(#:mode)
               '(fast)
               '("super.rkt"))

4.4 lambda函數(過程)

一個lambda表達式建立一個函數。在最簡單的情況,一個lambda表達式具有的表:

(lambda (arg-id ...)
  body ...+)

一個具有n個arg-id的lambda表接受n個參數:

> ((lambda (x) x)
   1)
1
> ((lambda (x y) (+ x y))
   1 2)
3
> ((lambda (x y) (+ x y))
   1)
#<procedure>: arity mismatch;
 the expected number of arguments does not match the given
number
  expected: 2
  given: 1
  arguments...:
   1
4.4.1 申明一個剩餘(rest)參數

一個lambda表達式也可以有這種表:

(lambda rest-id
  body ...+)

也就是說,一個lambda表達式可以有一個沒有被圓括号包圍的單個rest-id。所得到的函數接受任意數量的參數,并且這個參數放入一個綁定到rest-id的清單:

Examples:

> ((lambda x x)
   1 2 3)
'(1 2 3)
> ((lambda x x))
'()
> ((lambda x (car x))
   1 2 3)
1

帶有一個rest-id的函數經常使用apply函數調用另外的函數,它接受任意數量的參數。

Racket程式設計指南——4 表達式和定義
《apply函數》描述apply。

Examples:

(define max-mag
  (lambda nums
    (apply max (map magnitude nums))))
> (max 1 -2 0)
1
> (max-mag 1 -2 0)
2

lambda表還支援必需參數與一個rest-id組合:

(lambda (arg-id ...+ . rest-id)
  body ...+)

這個表的結果是一個函數,它至少需要與arg-id一樣多的參數,并且還接受任意數量的附加參數。

Examples:

(define max-mag
  (lambda (num . nums)
    (apply max (map magnitude (cons num nums)))))
> (max-mag 1 -2 0)
2
> (max-mag)
max-mag: arity mismatch;
 the expected number of arguments does not match the given
number
  expected: at least 1
  given: 0

一個rest-id變量有時稱為一個rest參數(rest argument),因為它接受函數參數的“剩餘(rest)”。

4.4.2 聲明可選(optional)參數

不隻是一個辨別符,一個lambda表中的一個參數(不僅是一個剩餘參數)可以用一個辨別符和一個預設值指定:

(lambda gen-formals
  body ...+)
gen-formals = (arg ...)
| rest-id
| (arg ...+ . rest-id)
arg = arg-id
| [arg-id default-expr]

表的一個參數[arg-id default-expr]是可選的。當這個參數不在一個應用程式中提供,default-expr産生預設值。default-expr可以引用任何前面的arg-id,并且下面的每個arg-id也必須應該有一個預設值。

Examples:

(define greet
  (lambda (given [surname "Smith"])
    (string-append "Hello, " given " " surname)))
> (greet "John")
"Hello, John Smith"
> (greet "John" "Doe")
"Hello, John Doe"
(define greet
  (lambda (given [surname (if (equal? given "John")
                              "Doe"
                              "Smith")])
    (string-append "Hello, " given " " surname)))
> (greet "John")
"Hello, John Doe"
> (greet "Adam")
"Hello, Adam Smith"
4.4.3 聲明關鍵字(keyword)參數

一個lambda表可以聲明一個參數來通過關鍵字傳遞,而不是通過位置傳遞。關鍵字參數可以與位置參數混合,而且預設值表達式可以提供給兩種參數:

Racket程式設計指南——4 表達式和定義
《關鍵字參數》介紹用關鍵字進行函數調用。
(lambda gen-formals
  body ...+)
gen-formals = (arg ...)
| rest-id
| (arg ...+ . rest-id)
arg = arg-id
| [arg-id default-expr]
| arg-keyword arg-id
| arg-keyword [arg-id default-expr]

由一個應用程式使用同一個arg-keyword提供一個參數指定為arg-keyword arg-id。關鍵字的位置——在參數清單中的辨別符配對與一個應用程式中的參數比對并不重要,因為它将通過關鍵字而不是位置與一個參數值比對。

(define greet
  (lambda (given #:last surname)
    (string-append "Hello, " given " " surname)))
> (greet "John" #:last "Smith")
"Hello, John Smith"
> (greet #:last "Doe" "John")
"Hello, John Doe"

一個arg-keyword [arg-id default-expr]參數指定一個帶一個預設值的關鍵字參數。

Examples:

(define greet
  (lambda (#:hi [hi "Hello"] given #:last [surname "Smith"])
    (string-append hi ", " given " " surname)))
> (greet "John")
"Hello, John Smith"
> (greet "Karl" #:last "Marx")
"Hello, Karl Marx"
> (greet "John" #:hi "Howdy")
"Howdy, John Smith"
> (greet "Karl" #:last "Marx" #:hi "Guten Tag")
"Guten Tag, Karl Marx"

lambda表不直接支援建立一個接受“rest”關鍵字的函數。要構造一個接受所有關鍵字參數的函數,使用make-keyword-procedure函數。這個函數支援make-keyword-procedure通過最先的兩個(按位置)參數中的并行清單接受關鍵字參數,然後來自一個應用程式的所有位置參數作為保留位置參數。

Racket程式設計指南——4 表達式和定義
《apply函數》介紹了keyword-apply。

Examples:

(define (trace-wrap f)
  (make-keyword-procedure
   (lambda (kws kw-args . rest)
     (printf "Called with ~s ~s ~s\n" kws kw-args rest)
     (keyword-apply f kws kw-args rest))))
> ((trace-wrap greet) "John" #:hi "Howdy")
Called with (#:hi) ("Howdy") ("John")
"Howdy, John Smith"
在《Racket參考》的“(lambda)”中提供了更多函數表達式的内容。
4.4.4 實參數量感覺函數:case-lambda

case-lambda表建立一個函數,它可以根據提供的參數數量而具有完全不同的行為。一個case-lambda表達式有這樣的表:

(case-lambda
  [formals body ...+]
  ...)
formals = (arg-id ...)
| rest-id
| (arg-id ...+ . rest-id)

每個[formals body ...+]類似于(lambda formals body ...+)。應用以case-lambda生成一個函數類似于應用一個lambda給比對給定參數數量的第一種情況。

Examples:

(define greet
  (case-lambda
    [(name) (string-append "Hello, " name)]
    [(given surname) (string-append "Hello, " given " " surname)]))
> (greet "John")
"Hello, John"
> (greet "John" "Smith")
"Hello, John Smith"
> (greet)
greet: arity mismatch;
 the expected number of arguments does not match the given
number
  given: 0

一個case-lambda函數不能直接支援可選參數或關鍵字參數。

4.5 定義:define

一個基本定義具為如下表:

(define id expr)

在這種情況下,id被綁定到expr的結果。

Examples:

(define salutation (list-ref '("Hi" "Hello") (random 2)))
> salutation
"Hi"
4.5.1 函數簡寫

define表還支援函數定義的一個簡寫:

(define (id arg ...) body ...+)

這是以下内容的簡寫:

(define id (lambda (arg ...) body ...+))

Examples:

(define (greet name)
  (string-append salutation ", " name))
> (greet "John")
"Hi, John"
(define (greet first [surname "Smith"] #:hi [hi salutation])
  (string-append hi ", " first " " surname))
> (greet "John")
"Hi, John Smith"
> (greet "John" #:hi "Hey")
"Hey, John Smith"
> (greet "John" "Doe")
"Hi, John Doe"

函數簡寫通過define也支援一個剩餘參數(rest argument)(即,一個最終參數以在一個清單中收集額外參數):

(define (id arg ... . rest-id) body ...+)

它是以下内容的一個簡寫:

(define id (lambda (arg ... . rest-id) body ...+))

Examples:

(define (avg . l)
  (/ (apply + l) (length l)))
> (avg 1 2 3)
2
4.5.2 柯裡函數簡寫

注意下面的make-add-suffix函數,它接收一個字元串并傳回另一個接受一個字元串的函數:

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

雖然不常見,但make-add-suffix的結果可以直接調用,就像這樣:

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

從某種意義上說,make-add-suffix是一個函數,接受兩個參數,但一次隻接受一個參數。一個函數,它接受它的參數的一些并傳回一個函數以接受更多,這種函數有時被稱為一個柯裡函數(curried function)。

使用define的函數簡寫表,make-add-suffix可以等效地編寫為:

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

這個簡寫反映了函數調用(make-add-suffix "!")的形态。define表更進一步支援定義反映嵌套函數調用的柯裡函數的一個簡寫:

(define ((make-add-suffix s2) s)
  (string-append s s2))
> ((make-add-suffix "!") "hello")
"hello!"
(define louder (make-add-suffix "!"))
(define less-sure (make-add-suffix "?"))
> (less-sure "really")
"really?"
> (louder "really")
"really!"

用于define的函數簡寫的完整文法如下所示:

(define (head args) body ...+)
head = id
| (head args)
args = arg ...
| arg ... . rest-id

這個簡寫的擴充有一個給定義中的每個head的嵌套lambda表,其最裡面的head與最外面的lambda對應。

4.5.3 多值和define-values

一個Racket表達式通常産生一個單獨的結果,但有些表達式可以産生多個結果。例如,quotient和remainder各自産生一個值,但quotient/remainder同時産生同樣的兩個值:

> (quotient 13 3)
4
> (remainder 13 3)
1
> (quotient/remainder 13 3)
4
1

如上所示,REPL在自己的行列印每一結果值。

多值函數可以依據values函數來實作,它接受任意數量的值并将它們作為結果傳回:

> (values 1 2 3)
1
2
3
(define (split-name name)
  (let ([parts (regexp-split " " name)])
    (if (= (length parts) 2)
        (values (list-ref parts 0) (list-ref parts 1))
        (error "not a <first> <last> name"))))
> (split-name "Adam Smith")
"Adam"
"Smith"

define-values表同時綁定多個辨別符到産生于一個單表達式的多個結果:

(define-values (id ...) expr)

由expr産生的結果數量必須與id的數量相比對。

Examples:

(define-values (given surname) (split-name "Adam Smith"))
> given
"Adam"
> surname
"Smith"

一個define表(不是一個函數簡寫)等價于一個帶有一個單個id的define-values表。

在《Racket參考》中的“(define)”部分提供了更多關于定義的内容。
4.5.4 内部定義

當一個句法表的文法指定body,那相應的表可以是一個定義或一個表達式。作為一個body的一個定義是一個内部定義(internal definition)。

隻要最後一個body是表達式,在一個body序列中的表達式和内部定義可以被混合。

例如, lambda的文法是:

(lambda gen-formals
  body ...+)

是以下面是文法的有效執行個體:

(lambda (f)                ; 沒有定義
  (printf "running\n")
  (f 0))
(lambda (f)                ; 一個定義
  (define (log-it what)
    (printf "~a\n" what))
  (log-it "running")
  (f 0)
  (log-it "done"))
(lambda (f n)              ; 兩個定義
  (define (call n)
    (if (zero? n)
        (log-it "done")
        (begin
          (log-it "running")
          (f n)
          (call (- n 1)))))
  (define (log-it what)
    (printf "~a\n" what))
  (call n))

在一個特定的body序列中的内部定義是互相遞歸的,也就是說,任何定義都可以引用任何其它定義——隻要這個引用在定義發生之前沒有實際被求值。如果一個定義被過早引用,一個錯誤就會發生。

Examples:

(define (weird)
  (define x x)
  x)
> (weird)
x: undefined;
 cannot use before initialization

内部定義的一個序列隻使用define很容易轉換為一個等效的letrec表(如同在下一節中介紹的)。然而,其它的定義表可以表現為一個body,包括define-values、struct(見《程式員定義的資料類型》)或define-syntax(見《宏》)。

在《Racket參考》文檔的“(intdef-body)”部分有内部定義更多知識點。

4.6 局部綁定

雖然内部define可用于局部綁定,Racket提供了三種表,它們給予程式員在綁定方面的更多控制:let、let*和letrec。

4.6.1 并行綁定:let
在《Racket參考》的“(let)”部分也有關于let的文檔。

一個let表綁定一組辨別符,每個對應某個表達式的結果,以在let主體中使用:

(let ([id expr] ...) body ...+)

id綁定”在并行(parallel)狀态中”。也就是說,在右手邊的expr裡面沒有id被綁定于任何id,但在body中所有的都能找到。id必須不同于其它彼此。

Examples:

> (let ([me "Bob"])
    me)
"Bob"
> (let ([me "Bob"]
        [myself "Robert"]
        [I "Bobby"])
    (list me myself I))
'("Bob" "Robert" "Bobby")
> (let ([me "Bob"]
        [me "Robert"])
    me)
eval:3:0: let: duplicate identifier
  at: me
  in: (let ((me "Bob") (me "Robert")) me)

事實上一個id的expr不知道它自己的綁定通常對封裝器有用,封裝器必須傳回舊的值:

> (let ([+ (lambda (x y)
             (if (string? x)
                 (string-append x y)
                 (+ x y)))]) ; 使用原來的 +
    (list (+ 1 2)
          (+ "see" "saw")))
'(3 "seesaw")

偶爾,let綁定的并行性便于交換或重排一組綁定:

> (let ([me "Tarzan"]
        [you "Jane"])
    (let ([me you]
          [you me])
      (list me you)))
'("Jane" "Tarzan")

let綁定以“并行”的特性并不意味着隐含同時發生求值。盡管綁定被延遲到所有expr被求值,expr是按順序求值的。

4.6.2 順序綁定:let*
在《Racket參考》的“(let)”部分也有關于let*的文檔。

let*的文法和let的一樣:

(let* ([id expr] ...) body ...+)

不同的是,每個id可在以後的expr使用中以及body中找到。此外,id不需要有差別,并且最近的綁定是可見的一個。

Examples:

> (let* ([x (list "Burroughs")]
         [y (cons "Rice" x)]
         [z (cons "Edgar" y)])
    (list x y z))
'(("Burroughs") ("Rice" "Burroughs") ("Edgar" "Rice" "Burroughs"))
> (let* ([name (list "Burroughs")]
         [name (cons "Rice" name)]
         [name (cons "Edgar" name)])
    name)
'("Edgar" "Rice" "Burroughs")

換言之,一個let*表等效于嵌套的let表,每一個帶有一個單獨的綁定:

> (let ([name (list "Burroughs")])
    (let ([name (cons "Rice" name)])
      (let ([name (cons "Edgar" name)])
        name)))
'("Edgar" "Rice" "Burroughs")
4.6.3 遞歸綁定:letrec
在《Racket參考》的“(let)”部分也有關于letrec的文檔。

letrec的文法也和let相同:

(letrec ([id expr] ...) body ...+)

而let使其綁定僅在body内被找到,let*使其綁定在任何後面的綁定expr内被找到,letrec使其綁定在所有其它expr——甚至更早的expr内被找到。換句話說,letrec綁定是遞歸的。

在一個letrec表中的expr經常大都是用于遞歸的以及互相遞歸的lambda表函數:

> (letrec ([swing
            (lambda (t)
              (if (eq? (car t) 'tarzan)
                  (cons 'vine
                        (cons 'tarzan (cddr t)))
                  (cons (car t)
                        (swing (cdr t)))))])
    (swing '(vine tarzan vine vine)))
'(vine vine tarzan vine)
> (letrec ([tarzan-near-top-of-tree?
            (lambda (name path depth)
              (or (equal? name "tarzan")
                  (and (directory-exists? path)
                       (tarzan-in-directory? path depth))))]
           [tarzan-in-directory?
            (lambda (dir depth)
              (cond
                [(zero? depth) #f]
                [else
                 (ormap
                  (λ (elem)
                    (tarzan-near-top-of-tree? (path-element->string elem)
                                              (build-path dir elem)
                                              (- depth 1)))
                  (directory-list dir))]))])
    (tarzan-near-top-of-tree? "tmp"
                              (find-system-path 'temp-dir)
                              4))
#f

當一個letrec表的expr是典型的lambda表達式時,它們可以是任何表達式。表達式按順序求值,而且在每個值被擷取後,它立即用相應的id關聯。如果一個id在其值準備就緒之前被引用,一個錯誤被引發,正如内部定義一樣。

> (letrec ([quicksand quicksand])
    quicksand)
quicksand: undefined;
 cannot use before initialization
4.6.4 命名let

一個命名let是一個疊代和遞歸表。它使用與局部綁定相同的文法關鍵字let,但在let之後的一個辨別符(而不是一個最近的開括号)觸發一個不同的解析。

(let proc-id ([arg-id init-expr] ...)
  body ...+)

一個命名let表等效于

(letrec ([proc-id (lambda (arg-id ...)
                     body ...+)])
  (proc-id init-expr ...))

也就是說,一個命名let綁定一個隻在函數主體中可見的函數辨別符,并且用一些初始表達式的值隐式調用函數。

Examples:

(define (duplicate pos lst)
  (let dup ([i 0]
            [lst lst])
   (cond
    [(= i pos) (cons (car lst) lst)]
    [else (cons (car lst) (dup (+ i 1) (cdr lst)))])))
> (duplicate 1 (list "apple" "cheese burger!" "banana"))
'("apple" "cheese burger!" "cheese burger!" "banana")
4.6.5 多值綁定:let-values,let*-values,letrec-values
在《Racket參考》的“(let)”部分也有關于多值綁定表的文檔。

以define-values同樣的方式綁定在一個定義中的多個結果(見《多值和define-values》),let-values、let*-values和letrec-values值綁定多個局部結果。

(let-values ([(id ...) expr] ...)
  body ...+)
(let*-values ([(id ...) expr] ...)
  body ...+)
(letrec-values ([(id ...) expr] ...)
  body ...+)

每個expr必須産生一樣多的對應于id的值。綁定的規則是和沒有-values表的表相同:let-values的id隻綁定在body裡,let*-values的id綁定在後面從句裡的expr裡,letrec-value的id被綁定給所有的expr。

Example:

> (let-values ([(q r) (quotient/remainder 14 3)])
    (list q r))
'(4 2)

4.7 條件

大多數函數都可用于分支,如<和string?,産生#t或#f。無論什麼情況,Racket的分支表以任何非#f值為真。我們說一個真值(true value)意味着#f值之外的任何值。

這個對“真值(true value)”的約定在#f能夠代替故障或表明不提供一個可選的值的地方與協定完全吻合 。(謹防過度使用這一技巧,記住一個異常通常對報告故障是一個更好的機制。)

例如,member函數具有雙重職責;它可以用來查找一個從一個特定條目開始的清單的尾部,或者它可以用來簡單地檢查一個項目是否存在于一個清單中:

> (member "Groucho" '("Harpo" "Zeppo"))
#f
> (member "Groucho" '("Harpo" "Groucho" "Zeppo"))
'("Groucho" "Zeppo")
> (if (member "Groucho" '("Harpo" "Zeppo"))
      'yep
      'nope)
'nope
> (if (member "Groucho" '("Harpo" "Groucho" "Zeppo"))
      'yep
      'nope)
'yep
4.7.1 簡單分支:if
在《Racket參考》裡的“(if)”部分有關于if的文檔。

在一個if表裡:

(if test-expr then-expr else-expr)

test-expr總是被求值。如果它産生任何非#f值,那麼then-expr被求值。否則,else-expr被求值。

一個if表必須既有一個then-expr也有一個else-expr;後者不是可選的。執行(或跳過)基于一個test-expr的副作用,使用when或unless,對此我們将在後邊《定序》部分描述。

4.7.2 組合測試:and和or
在《Racket參考》的“(if)”部分有關于and和or的文檔。

Racket的and和or是文法表,而不是函數。不像一個函數,如果前邊的一個求值确定了答案,and和or表會忽略後邊表達式的求值。

(and expr ...)

如果其所有expr産生#f,一個and表産生#f。否則,它從它最後的expr産生值。作為一個特殊的情況,(and)産生#t。

(or expr ...)

如果其所有的expr産生#f,and表産生#f。否則,它從它的expr第一個非#f值産生值。作為一個特殊的情況,(or)産生#f。

Examples:

> (define (got-milk? lst)
    (and (not (null? lst))
         (or (eq? 'milk (car lst))
             (got-milk? (cdr lst))))) ; 僅在需要時再發生。
> (got-milk? '(apple banana))
#f
> (got-milk? '(apple milk banana))
#t

如果求值達到一個and或or}表的最後的expr,那麼expr的值直接決定and或or}的結果。是以,最後的expr是在尾部的位置,這意味着上面的got-milk?函數在固定空間中運作。

Racket程式設計指南——4 表達式和定義
《尾遞歸》介紹尾部調用和尾部位置。
4.7.3 編鍊測試:cond

cond表編鍊了一系列的測試以選擇一個結果表達式。對于一個初步近式,cond文法如下:

在《Racket參考》裡的“(if)”部分也有關于cond的文檔。
(cond [test-expr body ...+]
      ...)

每個test-expr被按順序求值。如果它産生#f,相應的body被忽略,并且求值進行到下一個test-expr。一旦一個test-expr産生一個真值,它的body被求值以産生作為cond表的結果。并不再進一步對test-expr求值。

在一個cond裡最後的test-expr可用else代替。就求值而言,else作為一個#t的同義詞提供,但它闡明了最後的從句意味着捕獲所有剩餘的執行個體。如果else沒有被使用,那麼可能沒有test-expr産生一個真值;在這種情況下,該cond表達式的結果是#<void>。

Examples:

> (cond
   [(= 2 3) (error "wrong!")]
   [(= 2 2) 'ok])
'ok
> (cond
   [(= 2 3) (error "wrong!")])
> (cond
   [(= 2 3) (error "wrong!")]
   [else 'ok])
'ok
(define (got-milk? lst)
  (cond
    [(null? lst) #f]
    [(eq? 'milk (car lst)) #t]
    [else (got-milk? (cdr lst))]))
> (got-milk? '(apple banana))
#f
> (got-milk? '(apple milk banana))
#t

cond的完整文法包括另外兩種從句:

(cond cond-clause ...)
cond-clause = [test-expr then-body ...+]
| [else then-body ...+]
| [test-expr => proc-expr]
| [test-expr]

=>變體擷取其test-expr的真值結果并且傳遞給proc-expr的結果,proc-expr必須是有一個參數的一個函數。

Examples:

> (define (after-groucho lst)
    (cond
      [(member "Groucho" lst) => cdr]
      [else (error "not there")]))
> (after-groucho '("Harpo" "Groucho" "Zeppo"))
'("Zeppo")
> (after-groucho '("Harpo" "Zeppo"))
not there

一個從句隻包括一個test-expr是很少使用的。它捕獲test-expr的真值結果,并簡單地傳回這個結果給整個cond表達式。

4.8 定序

Racket程式員喜歡編寫盡可能少副作用的程式,因為純粹的函數式代碼更容易測試及組成更大的程式。然而,與外部環境的互動需要定序,例如寫入一個顯示器、打開一個圖形視窗或在磁盤上操作一個檔案時。

4.8.1 前效應:begin
在《Racket參考》的“(begin)”中也有關于begin的文檔。

一個begin表達式定序表達式:

(begin expr ...+)

expr被順序求值,并且除最後的expr結果外所有結果都被忽略。來自最後的expr結果作為begin表的結果,并且它是相對于begin表來說位于尾部位置。

Examples:

(define (print-triangle height)
  (if (zero? height)
      (void)
      (begin
        (display (make-string height #\*))
        (newline)
        (print-triangle (sub1 height)))))
> (print-triangle 4)
****
***
**
*

有多種表,比如lambda或cond支援一系列甚至沒有一個begin的表達式。這樣的狀态有時被叫做有一個隐含的begin。

Examples:

(define (print-triangle height)
  (cond
    [(positive? height)
     (display (make-string height #\*))
     (newline)
     (print-triangle (sub1 height))]))
> (print-triangle 4)
****
***
**
*

begin表在頂層(top level)、子產品層(module level)或僅在内部定義之後作為一個body是特定的。在這些位置,begin的上下文被拼接到周圍的上下文中,而不是形成一個表達式。

Example:

> (let ([curly 0])
    (begin
      (define moe (+ 1 curly))
      (define larry (+ 1 moe)))
    (list larry curly moe))
'(2 0 1)

這種拼接行為主要用于宏,我們稍後在《宏》中讨論。

4.8.2 後效應:begin0
在《Racket參考》的“(begin)”中也有關于begin0的文檔。

一個begin0表達式具有與一個begin表達式相同的文法:

(begin0 expr ...+)

不同的是begin0傳回第一個expr的結果,而不是最後的expr結果。begin0表對于實作發生在一個計算之後的副作用是有用的,尤其是在計算産生結果的一個未知數值的情況下。

Examples:

(define (log-times thunk)
  (printf "Start: ~s\n" (current-inexact-milliseconds))
  (begin0
    (thunk)
    (printf "End..: ~s\n" (current-inexact-milliseconds))))
> (log-times (lambda () (sleep 0.1) 0))
Start: 1531057885852.895
End..: 1531057885952.937
> (log-times (lambda () (values 1 2)))
Start: 1531057885953.197
End..: 1531057885953.242
1
2
4.8.3 if效應:when和unless
在《Racket參考》的“(when+unless)”部分也有關于when和unless的文檔。

when表将一個if樣式條件與對“then”子句且無“else”子句的定序組合:

(when test-expr then-body ...+)

如果test-expr産生一個真值,那麼所有的then-body被求值。最後的then-body結果是when表的結果。否則,沒有then-body被求值而且結果是#<void>。

unless是相似的:

(unless test-expr then-body ...+)

不同的是test-expr結果是相反的:如果test-expr結果為#f,then-body被求值。

Examples:

(define (enumerate lst)
  (if (null? (cdr lst))
      (printf "~a.\n" (car lst))
      (begin
        (printf "~a, " (car lst))
        (when (null? (cdr (cdr lst)))
          (printf "and "))
        (enumerate (cdr lst)))))
> (enumerate '("Larry" "Curly" "Moe"))
Larry, Curly, and Moe.
(define (print-triangle height)
  (unless (zero? height)
    (display (make-string height #\*))
    (newline)
    (print-triangle (sub1 height))))
> (print-triangle 4)
****
***
**
*

4.9 指派:set!

在《Racket參考》的“(set!)”中也有關于set!的文檔。

使用set!指派給一個變量:

(set! id expr)

一個set!表達式對expr求值并改變id(它必須限制在閉括号的環境内)為這個結果值。set!表達式自己的結果是#<void>。

Examples:

(define greeted null)
(define (greet name)
  (set! greeted (cons name greeted))
  (string-append "Hello, " name))
> (greet "Athos")
"Hello, Athos"
> (greet "Porthos")
"Hello, Porthos"
> (greet "Aramis")
"Hello, Aramis"
> greeted
'("Aramis" "Porthos" "Athos")
(define (make-running-total)
  (let ([n 0])
    (lambda ()
      (set! n (+ n 1))
      n)))
(define win (make-running-total))
(define lose (make-running-total))
> (win)
1
> (win)
2
> (lose)
1
> (win)
3
4.9.1 使用指派的指導原則

雖然使用set!有時是适當的,Racket風格通常不建議使用set!。下面的指導原則有助于解釋什麼時候使用set!是适當的。

  • 與任何現代語言一樣,指派給一個共享辨別符不是用傳遞一個參數給一個過程或取得其結果的替換。

    事實上很糟糕的例子:

    (define name "unknown")
    (define result "unknown")
    (define (greet)
      (set! result (string-append "Hello, " name)))
    > (set! name "John")
    > (greet)
    > result
    "Hello, John"
    好的例子:
    (define (greet name)
      (string-append "Hello, " name))
    > (greet "John")
    "Hello, John"
    > (greet "Anna")
    "Hello, Anna"
  • 對一個局部變量的一系列指派遠差于嵌套綁定。

    差的例子:

    > (let ([tree 0])
        (set! tree (list tree 1 tree))
        (set! tree (list tree 2 tree))
        (set! tree (list tree 3 tree))
        tree)
    '(((0 1 0) 2 (0 1 0)) 3 ((0 1 0) 2 (0 1 0)))
    好的例子:
    > (let* ([tree 0]
             [tree (list tree 1 tree)]
             [tree (list tree 2 tree)]
             [tree (list tree 3 tree)])
        tree)
    '(((0 1 0) 2 (0 1 0)) 3 ((0 1 0) 2 (0 1 0)))
  • 使用指派來從一個疊代中積累結果是不好的風格。通過一個循環參數積累更好。

    略差的示例:

    (define (sum lst)
      (let ([s 0])
        (for-each (lambda (i) (set! s (+ i s)))
                  lst)
        s))
    > (sum '(1 2 3))
    6
    好的示例:
    (define (sum lst)
      (let loop ([lst lst] [s 0])
        (if (null? lst)
            s
            (loop (cdr lst) (+ s (car lst))))))
    > (sum '(1 2 3))
    6
    更好(使用現有函數)示例:
    (define (sum lst)
      (apply + lst))
    > (sum '(1 2 3))
    6
    好的(一般方法)例子:
    (define (sum lst)
      (for/fold ([s 0])
                ([i (in-list lst)])
        (+ s i)))
    > (sum '(1 2 3))
    6
  • 對于在有狀态對象是必要或合适的情況下,那麼用set!實作對象的狀态很好的。

    好的例子:

    (define next-number!
      (let ([n 0])
        (lambda ()
          (set! n (add1 n))
          n)))
    > (next-number!)
    1
    > (next-number!)
    2
    > (next-number!)
    3

所有其它的情況都相同,不使用指派或突變的一個程式總是優于使用指派或突變的一個程式。雖然副作用應該被避免,然而,如果結果代碼可讀性明顯更高,或者如果實作了一個明顯更好的算法,則應該使用這些副作用。

可突變值的使用,如向量和哈希表,對一個程式的風格提出了比直接使用set!更少的懷疑。不過,在一個用vector-set!的程式中簡單替換set!顯然沒有改善程式的風格。

4.9.2 多值指派:set!-values
在《Racket參考》中的“(set!)”有關于set!-values的文檔。

set!-values表一次指派給多個變量,給一個生成值的一個适當數值的表達式:

(set!-values (id ...) expr)

這個表等價于使用let-values以從expr接收多個結果,然後将結果使用set!單獨指派給id。

Examples:

(define game
  (let ([w 0]
        [l 0])
    (lambda (win?)
      (if win?
          (set! w (+ w 1))
          (set! l (+ l 1)))
      (begin0
        (values w l)
        ; 交換雙方……
        (set!-values (w l) (values l w))))))
> (game #t)
1
> (game #t)
1
1
> (game #f)
1
2

4.10 引用:quote和'

在《Racket參考》中“(quote)”部分也有關于quote的文檔。

quote表産生一個常數:

(quote datum)

datum的文法在技術上被指定為read函數解析為一個單個元素的任何内容。quote表的值是相同的值,其read将産生給定的datum。

datum可以是一個符号、一個布爾值、一個數值、一個(字元或位元組)字元串、一個字元、一個關鍵字、一個空清單、一個包含更多類似值的點對(或清單),一個包含更多類似值的向量,一個包含更多類似值的哈希表,或者一個包含其它類似值的格子。

Examples:

> (quote apple)
'apple
> (quote #t)
#t
> (quote 42)
42
> (quote "hello")
"hello"
> (quote ())
'()
> (quote ((1 2 3) #("z" x) . the-end))
'((1 2 3) #("z" x) . the-end)
> (quote (1 2 . (3)))
'(1 2 3)

正如上面最後的示例所示,datum不需要比對一個值的格式化的列印表。一個datum不能作為一個以#<開始的列印呈現,是以不能是#<void>、#<undefined>或一個過程。

quote表很少用于一個datum,它是一個布爾值、數字或字元串本身,因為這些值的列印表已經可以用作常量。quote表更常用于符号和清單,當沒有被引用時,它具有其它含義(辨別符、函數調用等等)。

一個表達式:

'datum

是以下内容的一個簡寫

(quote datum)

這個簡寫幾乎總是用來代替quote。這個簡寫甚至應用于datum中,是以它可以生成一個包含quote的清單。

在《Racket參考》裡的“(parse-quote)”提供有更多關于'簡寫的内容。

Examples:

> 'apple
'apple
> '"hello"
"hello"
> '(1 2 3)
'(1 2 3)
> (display '(you can 'me))
(you can (quote me))

4.11 準引用:quasiquote和‘

在《Racket參考》中的“(quasiquote)”部分也有關于quasiquote的文檔。

quasiquote表類似于quote:

(quasiquote datum)

然而,對出現在datum之中的每個(unquote expr),expr被求值以産生一個替代unquote子表的值。

Example:

> (quasiquote (1 2 (unquote (+ 1 2)) (unquote (- 5 1))))
'(1 2 3 4)

此表可用于編寫根據特定模式建造清單的函數。

Examples:

> (define (deep n)
    (cond
      [(zero? n) 0]
      [else
       (quasiquote ((unquote n) (unquote (deep (- n 1)))))]))
> (deep 8)
'(8 (7 (6 (5 (4 (3 (2 (1 0))))))))

甚至可以以程式設計方式友善地構造表達式。(當然,第9次就超出了10次,你應該使用一個《宏》來做這個(第10次是當你學習了一本像《PLAI》那樣的教科書之後)。)

Examples:

> (define (build-exp n)
    (add-lets n (make-sum n)))
> (define (add-lets n body)
    (cond
      [(zero? n) body]
      [else
       (quasiquote
        (let ([(unquote (n->var n)) (unquote n)])
          (unquote (add-lets (- n 1) body))))]))
> (define (make-sum n)
    (cond
      [(= n 1) (n->var 1)]
      [else
       (quasiquote (+ (unquote (n->var n))
                      (unquote (make-sum (- n 1)))))]))
> (define (n->var n) (string->symbol (format "x~a" n)))
> (build-exp 3)
'(let ((x3 3)) (let ((x2 2)) (let ((x1 1)) (+ x3 (+ x2 x1)))))

unquote-splicing表和unquote相似,但其expr必須産生一個清單,而且unquote-splicing表必須出現在一個産生一個清單或一個向量的上下文裡。顧名思義,這個結果清單被拼接到它自己使用的上下文中。

Example:

> (quasiquote (1 2 (unquote-splicing (list (+ 1 2) (- 5 1))) 5))
'(1 2 3 4 5)

使用拼接,我們可以修改上邊我們的示例表達式的構造,以隻需要一個單個的let表達式和一個單個的+表達式。

Examples:

> (define (build-exp n)
    (add-lets
     n
     (quasiquote (+ (unquote-splicing
                     (build-list
                      n
                      (λ (x) (n->var (+ x 1)))))))))
> (define (add-lets n body)
    (quasiquote
     (let (unquote
           (build-list
            n
            (λ (n)
              (quasiquote
               [(unquote (n->var (+ n 1))) (unquote (+ n 1))]))))
       (unquote body))))
> (define (n->var n) (string->symbol (format "x~a" n)))
> (build-exp 3)
'(let ((x1 1) (x2 2) (x3 3)) (+ x1 x2 x3))

如果一個quasiquote表出現在一個封閉的quasiquote表裡,那這個内部的quasiquote有效地取消unquote表和unquote-splicing表的一層,結果一個第二層unquote或unquote-splicing表被需要。

Examples:

> (quasiquote (1 2 (quasiquote (unquote (+ 1 2)))))
'(1 2 (quasiquote (unquote (+ 1 2))))
> (quasiquote (1 2 (quasiquote (unquote (unquote (+ 1 2))))))
'(1 2 (quasiquote (unquote 3)))
> (quasiquote (1 2 (quasiquote ((unquote (+ 1 2)) (unquote (unquote (- 5 1)))))))
'(1 2 (quasiquote ((unquote (+ 1 2)) (unquote 4))))

上面的求值實際上不會像顯示那樣列印。相反,quasiquote和unquote的速記形式将被使用:`(即一個反引号)和,(即一個逗号)。同樣的簡寫可在表達式中使用:

Example:

> `(1 2 `(,(+ 1 2) ,,(- 5 1)))
'(1 2 `(,(+ 1 2) ,4))

unquote-splicing的簡寫形式是,@:

Example:

> `(1 2 ,@(list (+ 1 2) (- 5 1)))
'(1 2 3 4)

4.12 簡單分派:case

通過将一個表達式的結果與子句的值相比對,case表分派到一個從句:

(case expr
  [(datum ...+) body ...+]
  ...)

每個datum将使用equal?對比expr的結果,然後相應的body被求值。case表可以為N個datum在O(log N)時間内分派正确的從句。

可以給每個從句提供多個datum,而且如果任何一個datum比對,那麼相應的body被求值。

Example:

> (let ([v (random 6)])
    (printf "~a\n" v)
    (case v
      [(0) 'zero]
      [(1) 'one]
      [(2) 'two]
      [(3 4 5) 'many]))
'zero

一個case表的最後從句可以使用else,就像cond那樣:

Example:

> (case (random 6)
    [(0) 'zero]
    [(1) 'one]
    [(2) 'two]
    [else 'many])
'two

對于更一般的模式比對(但沒有分派時間保證),使用match,這個會在《模式比對》中介紹。

4.13 動态綁定:parameterize

在《Racket參考》中的“(parameterize)”部分也有關于parameterize的文檔。

parameterize表把一個新值和body表達式的求值過程中的一個參數parameter相結合:

(parameterize ([parameter-expr value-expr] ...)
  body ...+)
術語“參數”有時用于指一個函數的參數,但Racket中的“參數”在這裡有更具體的意義描述。

例如,error-print-width參數控制在錯誤消息中列印一個值的字元數:

> (parameterize ([error-print-width 5])
    (car (expt 10 1024)))
car: contract violation
  expected: pair?
  given: 10...
> (parameterize ([error-print-width 10])
    (car (expt 10 1024)))
car: contract violation
  expected: pair?
  given: 1000000...

一般來說,參數實作了一種動态綁定。make-parameter函數接受任何值并傳回一個初始化為給定值的新參數。應用參數作為一個函數傳回它的目前值:

> (define location (make-parameter "here"))
> (location)
"here"

在一個parameterize表裡,每個parameter-expr必須産生一個參數。在body的求值過程中,每一個指定的參數給出對應于value-expr的結果。當控制離開parameterize表——無論是通過一個正常的傳回、一個例外或其它逃逸——這個參數恢複到其先前的值:

> (parameterize ([location "there"])
    (location))
"there"
> (location)
"here"
> (parameterize ([location "in a house"])
    (list (location)
          (parameterize ([location "with a mouse"])
            (location))
          (location)))
'("in a house" "with a mouse" "in a house")
> (parameterize ([location "in a box"])
    (car (location)))
car: contract violation
  expected: pair?
  given: "in a box"
> (location)
"here"

parameterize表不是一個像let的綁定表;每次上邊location的使用都直接指向原始的定義。在parameterize主體被求值的整個時間内,一個parameterize表調整一個參數的值,即使對于參數的使用是在parameterize主體以外符合文本:

> (define (would-you-could-you?)
    (and (not (equal? (location) "here"))
         (not (equal? (location) "there"))))
> (would-you-could-you?)
#f
> (parameterize ([location "on a bus"])
    (would-you-could-you?))
#t

如果一個參數的一個使用是在一個parameterize的主體内部符合文本,但是在parameterize表産生一個值之前沒有被求值,那麼這個使用沒有明白這個被parameterize表所設定的值:

> (let ([get (parameterize ([location "with a fox"])
               (lambda () (location)))])
    (get))
"here"

一個參數的目前綁定可以通過用一個值作為一個函數調用這個參數來做必要的調整。如果一個parameterize已經調整了參數的值,那麼直接應用參數過程僅僅影響與活動parameterize相關的值:

> (define (try-again! where)
    (location where))
> (location)
"here"
> (parameterize ([location "on a train"])
    (list (location)
          (begin (try-again! "in a boat")
                 (location))))
'("on a train" "in a boat")
> (location)
"here"

使用parameterize通常更适合于強制更新一個參數值——對于用let綁定一個新變量的大多數相同原因是更好地使用set! (參見《指派:set!》)。

似乎變量和set!可以解決很多參數解決的相同問題。例如,lokation可以被定義為一個字元串,以及set!可以用來調整它的值:

> (define lokation "here")
> (define (would-ya-could-ya?)
    (and (not (equal? lokation "here"))
         (not (equal? lokation "there"))))
> (set! lokation "on a bus")
> (would-ya-could-ya?)
#t

然而,參數比set!提供了幾個關鍵的優點:

  • parameterize表有助于在控制因一個異常導緻的逃逸時自動重置一個參數的值。 添加異常處理器和其它表以重繞一個set!是比較繁瑣的。
  • 參數可以和尾調用很好地工作(請參閱《尾遞歸》)。在一個parameterize表最後的body相對于parameterize表來說是處于尾部位置。
  • 參數與線程恰當地工作(請參閱《Racket參考》"(threads)"部分)。parameterize表僅因為在目前線程中的求值而調整一個參數的值,以避免與其它線程競争。

繼續閱讀