Racket编程指南——4 表达式和定义(五)

本文详细介绍了Racket编程语言中的局部绑定,包括let、let*和letrec,阐述了它们在绑定控制和递归中的应用。同时,讲解了条件表达式if、and、or和cond的使用,以及如何通过begin、when和unless实现定序和副作用的控制。这些概念和工具对于编写高效、清晰的Racket代码至关重要。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

directory-list: could not open directory

  path: /var/tmp/systemd-private-601deb1a4a46441cae24498fbda

3c772-ModemManager.service-SoWuoP

  system error: 权限不够; errno=13

当一个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的副作用,使用whenunless,对此我们将在后边《定序》部分描述。

4.7.2 组合测试:andor

在《Racket参考》的“(if)”部分有关于andor的文档。

Racket的andor是语法表,而不是函数。不像一个函数,如果前边的一个求值确定了答案,andor表会忽略后边表达式的求值。

(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

如果求值达到一个andor}表的最后的expr,那么expr的值直接决定andor}的结果。因此,最后的expr是在尾部的位置,这意味着上面的got-milk?函数在固定空间中运行。

尾递归》介绍尾部调用和尾部位置。

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)

****

***

**

*

有多种表,比如lambdacond支持一系列甚至没有一个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: 1668430707527.6035

End..: 1668430707627.6519

0

> (log-times (lambda () (values 1 2)))

Start: 1668430707629.9336

End..: 1668430707629.9768

1

2

4.8.3 if效应:whenunless

在《Racket参考》的“(when+unless)”部分也有关于whenunless的文档。

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)

****

***

**

*

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值