使用 Let 创建局部变量

讲解 Racket 中 let 特殊形式的用法,它与 lambda 调用的等价关系,以及如何用它创建局部变量。

局部变量(local variable)是只存在于局部环境(local environment)中的变量。看一个例子:

(define (foo x)
  (define a 5)
  (+ x a))

这里的局部环境是由函数 foo 创建的环境(environment),局部变量是 a。注意,x 不是局部变量——尽管它在 foo 外面同样无法访问,但它的正式名称是形参(parameter)。

let 简介

特殊形式 let 本质上就是一次调用方式不同的 lambda 函数调用。例如,看下面这个 lambda 函数调用:

-> ((lambda (x y z) (+ x y x z)) 1 2 3)
7

它等价于下面这个 let 语句:

-> (let ((x 1) (y 2) (z 3)) (+ x y x z))
7

这什么时候会有用呢?答案只有两个字:局部变量。我们很少会单纯为了调用一个 lambda 函数而使用 let 语句;相反,我们用它在函数内部创建局部变量。

示例:多项式

假设我们想用 Racket 计算下面这个多项式,其中 x 和 y 为任意给定值:

f(x,y)=x(1+xy)2+y(1−y)+(1+xy)(1−y)f(x,y) = x(1+xy)^2 + y (1-y) + (1+xy)(1-y)

把这个难看的多项式原样改写成一个难看的过程:

(define (f x y)
  (+ (* x (+ 1 (square (* x y)))) (* y (- 1 y)) (* (+ 1 (* x y)) (- 1 y))))

啧。不过,我们可以做一些代换:

a=1+xy\displaystyle a = 1 + xy

b=1−y\displaystyle b = 1 -y

于是得到:

f(x,y)=xa2+yb+ab\displaystyle f(x,y) = xa^2 + yb + ab

好吧,我承认这样似乎好些了。用 Racket 写的话,我们可以定义一个辅助函数 f-helper,这样就可以使用代换了:

(define (f x y)
    (define (f-helper a b)
        (+ (* x (square a))
           (* y b)
           (* a b)))
    (f-helper (+ 1 (* x y))
              (- 1 y)))

花一分钟确认一下:这确实和前面 f 的定义做的是同一件事。如我们在上一节所学,我们其实并不需要在 f 内部额外定义一个函数,可以用 lambda 代替:

(define (f x y)
    ((lambda (a b)
        (+ (* x (square a))
           (* y b)
           (* a b)))
    (+ 1 (* x y))
    (- 1 y)))

可惜的是,即便经过这番代换和重组,代码还是有点乱。这时候就该 let 登场了!

(define (f x y)
   (let ((a (+ 1 (* x y)))
         (b (- 1 y)))
     (+ (* x (square a)) (* y b) (* a b))))

终于,我们得到了最初那个多项式函数 f 的一个更可读的版本。我们可以清楚地看到:先给 a 和 b 赋值,再把它们代入 let 语句的主体。

let:一般形式

let 语句的一般结构是:

(let ((<var1> <exp1>)
      (<var2> <exp2>)
      ...
      (<varn> <expn>))
  <body>)

请记住,从本质上说,它无非是一次 lambda 调用。上面的结构等价于:

((lambda (<var1> <var2> ... <varn>) <body>)
  <exp1> <exp2> ... <expn> )

在 Racket 解释器中试试这些表达式(以及更多表达式!)。

(注意:分号表示注释,Racket 会忽略分号之后该行的剩余内容。)

(define y 10)  

(let ((y 0)) y) ;; 注意,let 会覆盖全局变量

(let ((x 10)  
      (z x))   
    z) ;; 这行会报错,翻译成 lambda 看看为什么

(let ((a 1))  
  (let ((a 2))  
    (let ((a 3))  
       a))) ;; 嵌套 let 是合法的

(let ((test 'wait-what?))  
  5)  
test  ;; let 只在其主体内部绑定变量

(let ((a 1))  
  (+ a (let ((a 2))  
    (+ a (let ((a 3))  
            a))))) ;; 挑战:先猜猜最后这个表达式返回什么,再用解释器验证