编程语言导论/模板化编程
高阶函数促成了一种我们称之为模板化的编程风格。一个模板是一个带有"空洞"的算法。这些空洞必须用正确类型的操作填充。算法的骨架是固定的;但是,通过使用不同的操作,我们可以获得截然不同的行为。例如,让我们考虑 filter 的 SML 实现
fun filter _ nil = nil
| filter p (h::t) = if p h then h :: (filter p t) else filter p t
实现 filter 的算法必须对列表中的每个元素应用给定的单目操作符 p。然而,无论操作是什么,必须遵循的步骤总是相同的:遍历列表,对每个元素应用p。这个过程是一个模板,一个必须用实际操作填充才能工作的骨架。这个骨架可以与各种不同的操作符一起使用;因此,它具有很高的可重用性。
这种编程风格遵循开闭原则,通常在面向对象编程的背景下提及。filter 的实现是封闭的。换句话说,它可以与其他模块链接并使用,无需任何修改。但是,这种实现也对扩展开放。只要这些操作服从 filter 强制执行的类型规则,就可以将新的操作传递给该算法。即使我们假设将来可能会实现新的操作,只要这些操作符合 filter 强制执行的类型契约,filter 就可以在无需修改的情况下使用。
模板和偏应用的结合使程序员能够创建一些非常优雅的代码。例如,下面我们看到 SML 中快速排序算法的实现。在这个例子中,函数 grt
被用作函数工厂。每次必须处理新的枢轴时,即列表的第一个元素,我们通过调用 grt h
创建一个新的比较函数。如果我们让比较操作(在本例中为大于)开放,我们可以使该函数更加通用。通过传递不同的操作符,比如小于,我们将获得一个算法,该算法以降序而不是升序对整数进行排序。
fun grt a b = a > b
fun qsort nil = nil
| qsort (h::t) = (qsort (filter (grt h) t)) @ [h] @ (qsort (filter (leq h) t))
一些编程语言不提供高阶函数。这个家族中最著名的成员是Java。然而,模板也可以在这门语言中实现。Java 通过继承和子类型多态性的强大组合来弥补缺乏高阶函数的不足。例如,我们将展示如何在 Java 中实现 map 骨架。下面的代码是一个抽象类。Mapper
定义了一个必须由扩展它的类实现的方法 apply
。Mapper
还定义了一个具体的方法 map
。该方法完全实现了映射算法,并在其主体内部调用 apply
。但是,apply
的实现是开放的。
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
public abstract class Mapper<A, B> {
public abstract B apply(A e);
public final List<B> map(final List<A> l) {
List<B> retList = new LinkedList<B>();
Iterator<A> it = l.iterator();
while (it.hasNext()) {
retList.add(apply(it.next()));
}
return retList;
}
}
为了使用这个骨架,开发人员必须通过称为继承的机制来扩展它。如果类A扩展了另一个类B,那么我们称A为B的子类。例如,下面的类是Mapper
的一个子类,它实现了一个将列表元素增量化的函数
public class Incrementer extends Mapper<Integer, Integer> {
@Override
public Integer apply(Integer e) { return e + 1; }
}
类Incrementer
将一个整数列表映射到一个新的整数列表。下面的代码片段演示了如何使用这个类的实例来递增整数列表中的每个元素。如我们所见,在没有高阶函数的语言中模拟模板的整体过程相当冗长。
List<Integer> l0 = new LinkedList<Integer>();
for (int i = 0; i < 16384; i++) {
l0.add(i);
}
Mapper<Integer, Integer> m = new Incrementer();
List<Integer> l1 = m.map(l0);