嵌套映射

通过两次调用 map 实现嵌套映射,并用 flatmap 把列表的列表压平。

序列中的嵌套映射

在上一小节中,我们看到了如何组合 enumerate、map、filter 和 accumulate 来写出更复杂的函数。本小节我们将探讨嵌套映射(nested mapping)的一个例子:对列表调用两次 map。

棋盘网格

棋盘网格

Jack 是跳棋的超级粉丝。他想写一个函数,列出 4×4 棋盘上所有的坐标。更具体地说,他希望函数输出:

( (1 . 1) (1 . 2) (1 . 3) (1 . 4)
  (2 . 1) (2 . 2) (2 . 3) (2 . 4)
  (3 . 1) (3 . 2) (3 . 3) (3 . 4)
  (4 . 1) (4 . 2) (4 . 3) (4 . 4) )

注意,坐标表示为一对 x 和 y 坐标,代码输出由这些坐标组成的列表。我们将一步步演示 Jack 如何用已经学过的列表操作技术写出这个函数。

棋盘网格:第一行

棋盘网格

首先,我们把问题分成一小块,自底向上推进:写一些代码,返回第一行的坐标列表,即 ( (1 . 1) (2 . 1) (3 . 1) (4 . 1) )。怎么做到呢?我们发现 x 坐标从 1 开始、到 4 结束,而 y 坐标始终是 1。所以,如果我们有列表 (1 2 3 4),就可以把每个元素与 1 cons 起来。可以这样写:

> (map (lambda (x) (cons x 1))
     (enumerate 1 4))
((1 . 1) (2 . 1) (3 . 1) (4 . 1))

目前为止一切顺利。Jack 很开心。

棋盘网格:所有行

棋盘网格

我们已经有了返回第一行坐标列表的代码。既然只有 4 行,技术上我们可以给每行复制一份。

(map    (lambda (x) (cons x **1**))
        (enumerate 1 4))

(map    (lambda (x) (cons x **2**))
        (enumerate 1 4))

(map    (lambda (x) (cons x **3**))
        (enumerate 1 4))

(map    (lambda (x) (cons x **4**))
        (enumerate 1 4))

这倒也行,但大家都知道,复制粘贴代码通常不是好主意。(万一棋盘是 1000×1000 呢?)我们希望保留相同的部分,改动越少越好。注意,第 1、2、3、4 行代码唯一的区别,就是那个用来 cons 的数字。我们可以套用之前的方法:

(map (lambda (y) (map (lambda (x) (cons x y))
                      (enumerate 1 4)))
     (enumerate 1 4))

请注意,内层 λ 负责一行中的每一格,外层 λ 负责棋盘上的每一行。万岁!我们搞定了,对吧?

棋盘网格:压平

棋盘网格

这是我们运行当前代码得到的结果:

> (map (lambda (y)
        (map (lambda (x) (cons x y))
             (enumerate 1 4)))
      (enumerate 1 4))
( ((1 . 1) (2 . 1) (3 . 1) (4 . 1))
  ((1 . 2) (2 . 2) (3 . 2) (4 . 2))
  ((1 . 3) (2 . 3) (3 . 3) (4 . 3))
  ((1 . 4) (2 . 4) (3 . 4) (4 . 4)) )

这看起来与我们想要的结果惊人地相似:

( (1 . 1) (2 . 1) (3 . 1) (4 . 1)
  (1 . 2) (2 . 2) (3 . 2) (4 . 2)
  (1 . 3) (2 . 3) (3 . 3) (4 . 3)
  (1 . 4) (2 . 4) (3 . 4) (4 . 4) )

区别在哪里?我们当前的代码返回的是坐标的列表的列表,而我们想要的是坐标的列表。那么,如何”压平”这个列表呢?我们可以用 append 调用 accumulate:

(accumulate append
            nil
            (map (lambda (y)
                    (map (lambda (x) (cons x y)
                (enumerate 1 4))
            (enumerate 1 4))

Flatmap

用 append 调用 accumulate 非常常见,我们把这个过程实现为 flatmap:

(define (flatmap proc seq)
  (accumulate append nil (map proc seq)))

有了这个定义,我们终于可以写出 Jack 想要的函数了:

(flatmap (lambda (y)
             (map (lambda (x) (cons x y))
                  (enumerate 1 4)))
         (enumerate 1 4))

要点总结

当你想遍历某个列表并把它们的元素配对时,嵌套映射会很有用。为了让函数正确无误,强烈建议像我们这里一样把问题分解开。flatmap 是一个”压平”列表的列表的函数。