6.4 示例:实用函数 (Example: Utilities)

2.6 节提到过,Lisp 大部分是由 Lisp 函数组成,这些函数与你可以自己定义的函数一样。这是程序语言中一个有用的特色:你不需要改变你的想法来配合语言,因为你可以改变语言来配合你的想法。如果你想要 Common Lisp 有某个特定的函数,自己写一个,而这个函数会成为语言的一部分,就跟内置的 +eql 一样。

有经验的 Lisp 程序员,由上而下(top-down)也由下而上 (bottom-up)地工作。当他们朝着语言撰写程序的同时,也打造了一个更适合他们程序的语言。通过这种方式,语言与程序结合的更好,也更好用。

写来扩展 Lisp 的操作符称为实用函数(utilities)。当你写了更多 Lisp 程序时,会发现你开发了一系列的程序,而在一个项目写过许多的实用函数,下个项目里也会派上用场。

专业的程序员常发现,手边正在写的程序,与过去所写的程序有很大的关联。这就是软件重用让人听起来很吸引人的原因。但重用已经被联想成面向对象程序设计。但软件不需要是面向对象的才能重用 ── 这是很明显的,我们看看程序语言(换言之,编译器),是重用性最高的软件。

要获得可重用软件的方法是,由下而上地写程序,而程序不需要是面向对象的才能够由下而上地写出。实际上,函数式风格相比之下,更适合写出重用软件。想想看 sort 。在 Common Lisp 你几乎不需要自己写排序程序; sort 是如此的快与普遍,以致于它不值得我们烦恼。这才是可重用软件。

  1. (defun single? (lst)
  2. (and (consp lst) (null (cdr lst))))
  3. (defun append1 (lst obj)
  4. (append lst (list obj)))
  5. (defun map-int (fn n)
  6. (let ((acc nil))
  7. (dotimes (i n)
  8. (push (funcall fn i) acc))
  9. (nreverse acc)))
  10. (defun filter (fn lst)
  11. (let ((acc nil))
  12. (dolist (x lst)
  13. (let ((val (funcall fn x)))
  14. (if val (push val acc))))
  15. (nreverse acc)))
  16. (defun most (fn lst)
  17. (if (null lst)
  18. (values nil nil)
  19. (let* ((wins (car lst))
  20. (max (funcall fn wins)))
  21. (dolist (obj (cdr lst))
  22. (let ((score (funcall fn obj)))
  23. (when (> score max)
  24. (setf wins obj
  25. max score))))
  26. (values wins max))))

图 6.1 实用函数

你可以通过撰写实用函数,在程序里做到同样的事情。图 6.1 挑选了一组实用的函数。前两个 single?append1 函数,放在这的原因是要演示,即便是小程序也很有用。前一个函数 single? ,当实参是只有一个元素的列表时,返回真。

  1. > (single? '(a))
  2. T

而后一个函数 append1cons 很像,但在列表后面新增一个元素,而不是在前面:

  1. > (append1 '(a b c) 'd)
  2. (A B C D)

下个实用函数是 map-int ,接受一个函数与整数 n ,并返回将函数应用至整数 0n-1 的结果的列表。

这在测试的时候非常好用(一个 Lisp 的优点之一是,互动环境让你可以轻松地写出测试)。如果我们只想要一个 09 的列表,我们可以:

  1. > (map-int #'identity 10)
  2. (0 1 2 3 4 5 6 7 8 9)

然而要是我们想要一个具有 10 个随机数的列表,每个数介于 0 至 99 之间(包含 99),我们可以忽略参数并只要:

  1. > (map-int #'(lambda (x) (random 100))
  2. 10)
  3. (85 50 73 64 28 21 40 67 5 32)

map-int 的定义说明了 Lisp 构造列表的标准做法(idiom)之一。我们创建一个累积器 acc ,初始化是 nil ,并将之后的对象累积起来。当累积完毕时,反转累积器。 [1]

我们在 filter 中看到同样的做法。 filter 接受一个函数与一个列表,将函数应用至列表元素上时,返回所有非 nil 元素:

  1. > (filter #'(lambda (x)
  2. (and (evenp x) (+ x 10)))
  3. '(1 2 3 4 5 6 7))
  4. (12 14 16)

另一种思考 filter 的方式是用通用版本的 remove-if

图 6.1 的最后一个函数, most ,根据某个评分函数(scoring function),返回列表中最高分的元素。它返回两个值,获胜的元素以及它的分数:

  1. > (most #'length '((a b) (a b c) (a)))
  2. (A B C)
  3. 3

如果平手的话,返回先驰得点的元素。

注意图 6.1 的最后三个函数,它们全接受函数作为参数。 Lisp 使得将函数作为参数传递变得便捷,而这也是为什么,Lisp 适合由下而上程序设计的原因之一。成功的实用函数必须是通用的,当你可以将细节作为函数参数传递时,要将通用的部份抽象起来就变得容易许多。

本节给出的函数是通用的实用函数。可以用在任何种类的程序。但也可以替特定种类的程序撰写实用函数。确实,当我们谈到宏时,你可以凌驾于 Lisp 之上,写出自己的特定语言,如果你想这么做的话。如果你想要写可重用软件,看起来这是最靠谱的方式。