使用 Lambda 构造过程

介绍 Racket 中 lambda 匿名函数的语法结构与调用方式,并用它改写过程定义。

先来看看下面 sum-doubles 的定义。它接受两个数 a 和 b,返回 a 到 b 之间所有数翻倍之后的和:

(define (sum-doubles a b)
    (define (double x) (* 2 x))
    (if (> a b)
        0
        (+ (double a) (sum-doubles (+ a 1) b))))

由于系统没有为我们预定义 double 函数,我们不得不在 sum-doubles 的定义内部自己定义一个。

但这么做实在太浪费了!在 sum-doubles 的定义之外,我们永远用不到这个 double 函数。有没有一种快捷、简便的方法,不必先定义具名函数再去应用它,就能创建自定义函数呢?

Lambda:匿名函数

答案是肯定的。让我们来认识 lambda 函数,也就是所谓的匿名函数(anonymous function)。这些神秘的函数将在后续课程讨论的概念中大显身手。

lambda 的一般形式如下:

(lambda (<param1> <param2> ... <paramn>) <body>)

我们来拆解一下。在括号之内,主要有三个部分:

  • 标签 lambda:告诉 Racket 这是一个 lambda 函数;
  • 形参列表(parameter list):想要多少个形参都可以;
  • 主体(body):形参列表之后的所有内容。

例如,过程 double 可以用下面这个 lambda 函数来表示:

(lambda (x) (* 2 x))

换句话说,

(define double (lambda (x) (* 2 x)))

等价于:

(define (double x) (* 2 x))

描述 lambda 时,通常的说法是:「参数为 [params]、返回 [body] 的函数」。例如:「double 是参数为 x、返回 (* 2 x) 的函数。」

调用 Lambda

我们可以像调用用 define 创建的过程那样调用 lambda 函数。调用 lambda 的一般形式如下:

((lambda (<param1> <param2> ... <paramn>) <body>) <arg1> <arg2> ... <argn>)

如果我们想以匿名函数的方式调用 (double 5),按照替换模型(substitution model),会得到如下结果:

-> ((lambda (x) (* 2 x)) 5)
-> ((lambda (x) (* 2 5)) 5)
-> (* 2 5)
10

这里发生了什么?调用 lambda 函数时,第一个实参(argument)对应第一个形参,第二个实参对应第二个形参,……,第 n 个实参对应第 n 个形参。然后,在主体中,每个形参的每次出现都会被替换为相应的实参。

我们用一个例子表达式来说明:

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

在 lambda 的主体中,我们把 x 的每次出现都替换为 1,把每个 y 替换为 2,把每个 z 替换为 3。

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

现在,我们可以把 sum-doubles 重写成这样:

(define (sum-doubles a b)
    (if (> a b)
        0
        (+ ((lambda (x) (* 2 x)) a) (sum-doubles (+ a 1) b))))

笔记

创建一个 lambda 所返回的值是一个过程,与用 define 创建出来的过程完全一样。

在 Racket 解释器中试试这些表达式:

(lambda (x) (+ x 3))  
((lambda (x) (+ x 3)) 7)  
(define add3 (lambda (x) (+ x 3)))  
(add3 7)

(define (square x) (* x x))   
(square 5)   
(define sq (lambda (x) (* x x)))   
(sq 5)   
((lambda (x y) (+ (* 2 x) (* 5 y))) (* 100 100) (* 5 2))