序列作为常规接口

把枚举、映射、过滤和累加组合成常规接口,对序列操作进行高层抽象。

作为抽象的序列

我们来进一步探讨抽象(abstraction)的概念。使用抽象的一大好处是它能让代码更整洁、可读性更高。我们为序列编写的一些函数,可以用高阶函数(higher-order function)加以推广和抽象。这个思路可以概括为以下几步:

  • 在代码中找出反复出现的模式
  • 用高阶函数抽象出模式中的每个元素
  • 用这个抽象重新定义我们的代码

下面两个示例函数能帮助演示这个思路。

sum-odd-squares 接受一棵包含数字的树,把其中每个奇数元素的平方相加:

(define (sum-odd-squares tree)
  (cond ((null? tree) 0)  
        ((not (pair? tree))
         (if (odd? tree) (square tree) 0))
        (else (+ (sum-odd-squares (car tree))
                 (sum-odd-squares (cdr tree))))))

even-fibs 接受一个数 n,返回不超过 n 的偶数斐波那契数组成的列表:

(define (even-fibs n)
  (define (next k)
    (if (> k n)
        nil
        (let ((f (fib k)))
          (if (even? f)
              (cons f (next (+ k 1)))
              (next (+ k 1))))))
  (next 0))

乍看这两个函数,我们可能会说:“这两个函数毫无共同之处!“诚然,它们看起来完全不同,但它们的逻辑其实相同:

信号流图

我们思路的第一步是在代码中找出反复出现的模式。根据前几课对递归的描述,你可能会用基本情况和递归调用来拆解 sum-odd-squares 和 even-fibs。现在,让我们换个角度看看每个函数分别做了什么:

sum-odd-squares:

  • 枚举(enumerate)一棵树的所有叶子
  • 过滤(filter)掉数据为偶数的节点,只留下奇数值的节点
  • 把函数 square 映射(map)到剩下的每个节点,最后
  • 从 0 开始,用加法把结果累加(accumulate)起来。

even-fibs:

  • 枚举从 0 到 n 的所有整数
  • 把函数 fib 映射到每个整数上
  • 过滤掉奇数,只留下偶数斐波那契数,最后
  • 用 cons 从空列表开始累加结果。

我们看到了什么模式?起初看似截然不同的两个函数,现在可以归结为四个主要部分:枚举、过滤、累加和计算。这很棒,因为现在我们可以用高阶函数来抽象代码了。这就到了抽象思路的第二步。不过在此之前,我们先过一遍几个高阶函数。

Map

我们在第 4 课讲过 map 这个高阶函数。你可以回去快速复习一下。

Filter

filter 接受两个参数——谓词 predicate 和序列 sequence——返回序列中满足 predicate 的元素组成的序列。

(define (filter predicate sequence)
  (cond ((null? sequence) nil)
        ((predicate (car sequence))
         (cons (car sequence)
               (filter predicate (cdr sequence))))
        (else (filter predicate (cdr sequence)))))

测试理解

下面这些表达式分别返回什么?

(filter (lambda (x) (= (remainder x 2) 0)) (list 0 1 2 3 4 5))

(filter equal? '(bongo celia momo laval laburrita bongo))

选项(第一问):

  • nil:提示:这个 λ 函数在检查什么?
  • (0 1 2):提示:((lambda (x) (= (remainder x 2) 0)) 4) 返回什么?
  • (0 2 4):正确!(✅)这个表达式过滤掉了序列中所有不能被 2 整除的数。
  • Error:这个表达式在语法上是正确的。

选项(第二问):

  • nil:提示:这个谓词在检查什么?
  • (bongo celia momo laval laburrita bongo):提示:equal? 接受几个参数?
  • (bongo):提示:谓词应该有几个参数?
  • Error:正确!(✅)equal? 接受两个参数,但 filter 中的谓词每次只检查序列中的一个元素。这会调用 (equal? bongo),从而产生错误。

Accumulate

accumulate 接受一个操作 op、一个初始值 initial 和一个 sequence。从 initial 开始,accumulate 用 op 把 sequence 中的所有值组合成一个值。示例:

> (accumulate + 0 '(1 2 3 4 5))
15
> (accumulate append null '((1 2) (3 4) (5 6)))
(1 2 3 4 5 6)

accumulate 的定义如下:

(define (accumulate op initial sequence)
  (if (null? sequence)
      initial
      (op (car sequence)
          (accumulate op initial (cdr sequence)))))

这个高阶函数的工作原理可能有点费解,我们把求值(evaluation)步骤明确写出来:

考虑表达式:

(accumulate + 0 (list 1 2 3 4 5))

递归步骤如下:

(+ 1 (accumulate + 0 (list 2 3 4 5)))

(+ 1 (+ 2 (accumulate + 0 (list 3 4 5))))

(+ 1 (+ 2 (+ 3 (accumulate + 0 (list 4 5)))))

(+ 1 (+ 2 (+ 3 (+ 4 (accumulate + 0 (list 5))))))

(+ 1 (+ 2 (+ 3 (+ 4 (+ 5 (accumulate + 0 (list)))))))

(+ 1 (+ 2 (+ 3 (+ 4 (+ 5 0)))))

(+ 1 (+ 2 (+ 3 (+ 4 5))))

(+ 1 (+ 2 (+ 3 9)))

(+ 1 (+ 2 12))

(+ 1 14)

15

Enumerate

enumerate 是做什么的?enumerate 生成一个由元素组成的序列/列表。我们对 filter、map 和 accumulate 的定义都是针对序列的,但请注意,我们的函数之一 sum-odd-squares 是作用在树上的。与其给 accumulate、map 和 filter 各做多个版本,不如只用不同的 enumerate 函数来区分它们。

列表的 Enumerate

Enumerate 会给定上下界,返回一个列表。

  • (enumerate-interval 0 5) 返回 (0 1 2 3 4 5)
  • (enumerate-interval 10 13) 返回 (10 11 12 13)

你可以这样定义 enumerate(针对列表):

(define (enumerate-interval low high)
  (if (> low high)
      nil
      (cons low (enumerate-interval (+ low 1) high))))

树的 Enumerate

对于树版本的 enumerate,我们需要一个函数,接受一棵树,返回包含所有叶子的列表,以便与我们其余的高阶函数兼容。

(define (enumerate-tree tree)
  (cond ((null? tree) nil)
        ((not (pair? tree)) (list tree))
        (else (append (enumerate-tree (car tree))
                      (enumerate-tree (cdr tree))))))

把一切组合起来

到这里,我们的抽象思路来到了最后一步。有了定义好的所有辅助函数,我们可以写出更模块化、可读性更高、更紧凑的 sum-odd-squares 和 even-fibs 版本:

(define (sum-odd-squares tree)
  (accumulate +
              0
              (map square
                   (filter odd?
                           (enumerate-tree tree)))))

我们在这里做了什么?我们找出树中所有的叶子(枚举),保留所有奇数(过滤),对剩下的每个数求平方(映射),再把结果加起来(累加)。

类似地,我们可以这样定义 even-fibs:

(define (even-fibs n)
  (accumulate cons
              nil
              (filter even?
                      (map fib
                           (enumerate-interval 0 n)))))

这次又发生了什么?我们从 0 到 n 生成一个列表(枚举),求出它们各自的斐波那契数(映射),保留偶数(过滤),再把它们组成一个列表(累加)。

要点总结

借助 map、filter、accumulate 和 enumerate 的不同组合,序列为抽象提供了强大的基础。即便是像我们这里用作示例的、看起来结构迥异的函数,也可以用相似的过程信号(process signal)来拆解。