Lisp 系的语言比起编程更像数学一点——过程是名副其实的「函数」表达式,程序的运行是在对其求值;整个程序可以按照求值的代换模型展开成一棵树。从这个角度来编程解决问题,不考虑变量和循环之类的,是一种全新的思路;比如对过程定义产生的新理解。

问题

计算机程序的构造和解释》中的 1-28 题为了使用 Miller-Rabin 检验来确定数 是否是素数,要求修改之前使用过的,用于计算 的递归过程 expmod

(define (expmod base exp m)
    (cond
        ((= exp 0) 1)
        ((even? exp)
            (remainder
                (square (expmod base (/ exp 2) m))
                m
            )
        )
        (else
            (remainder
                (* base (expmod base (- exp 1) m))
                m
            )
        )
    )
)

这个过程在 exp 为奇数时将结果乘以 baseexp 为偶数时对结果求平方;求平方的方法使得它计算同余幂只需要 的步骤。

修改这个过程,使其在求平方的步骤中检查是否遇到了「 的非平凡平方根」,即是否

若上式对某一 成立,那么 就不是素数;若在任一时刻找到了这样的平方根,过程应当返回 0 ,否则返回

变量

很容易就能写出满足上面的条件的、递归的 C 语言函数:

int expmod (int base, int exp, int m)
{
    if (exp == 0) {
        return 1;
    } else if (exp % 2 == 0) {
        int r = expmod (base, exp / 2, m);
        int x = (r * r) % m;
        if (r == 1 || r == m - 1) {
            return x;
        } else {
            if (x == 1) {
                return 0;
            } else {
                return x;
            }
        }
    } else {
        return (expmod (base, exp - 1, m) * base) % m;
    }
}

在上面的函数中,一个很重要的部分就是使用了变量 rx 来保存递归返回的结果 。如果不保存,就需要多次调用函数。

如果在 Scheme 中,不使用中间变量而只考虑代换求值,会得到下面的树:

总之就是一棵有很多冗余的树

上图的过程在所有需要的地方都重新递归调用了所需的过程,这导致了在指数 exp 为偶数的分支上,有一棵子树 remainder 被反复求值。图中在条件判断过程中求值的 remainder 子树被标为蓝色,在结果计算过程中求值的 remainder 子树被标为红色。

remainder 子树的反复求值增加了递归函数的时间复杂度:一个函数要调用自身 7 次,这是不可接受的。在 C 中这个函数只被调用了一次,在此之后用变量存储它的返回值以便继续使用。但是如果不使用变量呢?

过程定义

Scheme 给出的解法是,在所有重复的 remainder 子树的公共祖先上,将它的后代重新定义为过程;这个新定义的过程作为原来的 expmod 的参数,而递归调用的 expmod 作为这个新定义的过程的参数。例如,将公共祖先 (even? exp) 结点的下方的分支定义为过程 get-result(这个过程可以理解为当 exp 为偶数时,取得所返回的结果),而 remainder 子树的值作为传给 get-result 过程的参数 r

基本没有冗余的 get-result

通过这种方式,改写后的 expmod 是这样的:

没有冗余的 expmod

在这个 expmod 中,由于调用了 get-result ,使得 remainder 子树的求值次数从 7 次变回了 1 次。这就是「过程定义」的解法。

这样的方法还可以继续使用,比如在 get-result 中求值了两次的remaindersquare 过程,可以通过再重新定义过程 test(测试平方是否与 1 同余)来优化。

真的没有冗余的 get-result

如果一个程序意义上的函数调用只允许传参数,而不在函数内部定义变量或更改变量的值(与此同时也禁用了循环——因为循环要求「状态」的改变),那它就类似数学意义上的函数:传入参数的类型是函数的定义域,返回值的类型是函数的值域。类似 ifcond 的条件选择则是对输入进行分段。函数调用的嵌套只不过是复合函数而已。

附录

正经解法的 Scheme 代码(使用我流括号缩进):

(define (expmod base exp m)
    (define (get-result r)
        (define (test x)
            (if (= x 1)
                0
                x
            )
        )
        (cond
            ((= r 0) 0)
            ((or (= r 1) (= r (- m 1)))
                (remainder (square r) m)
            )
            (else
                (test
                    (remainder (square r) m)
                )
            )
        )
    )
    (cond
        ((= exp 0) 1)
        ((even? exp)
            (get-result
                (expmod base (/ exp 2) m)
            )
        )
        (else
            (remainder
                (* base (expmod base (- exp 1) m))
                m
            )
        )
    )
)

如果人们学习 Lisp 系的障碍只是括号堆一起看着烦的话,让它们不要堆一起就可以了