Tuesday, June 28, 2011

How to understand recursion

这是从 ANSI Common Lisp 里看到的一段。本来想在豆瓣上写个读书笔记的,但是我觉得这个太重要了,应该写篇文章来记录一下。

很多人(包括我)觉得递归难以理解,因为我们大脑里对“函数”的模型不对。基于这种模型去思考,整个递归过程很容易变成一团浆糊。

那我们潜意识中是怎样对待“函数”的呢?书里一般会说函数是一种抽象,所以我们就把函数当作一个黑盒子,或者按照 Paul 的说法,当作一个机器。参数是这个机器的原材料;机器有时候会把部分工作外包给别的机器;最后的产品被组装起来,也就是函数的返回值。这就是我们大脑里的模型。

遇到递归函数的时候我们还是这样思考的,但是现在,这种抽象反而让事情更难以理解。一个机器已经在运转了,怎么还能把一部分活儿外包给自己?它们生成的产品又是怎么组装到一块的?

比如这样一个简单的递归函数,判断一个对象是否在一个列表中:

def member(obj, lst):
    if not lst:
        return False
    if lst[0] == obj:
        return lst
    return member(obj, lst[1:])

按照以前的习惯,我的大脑会尝试着展开每次递归调用。结果不必说,一团乱麻。

Paul 讲了一个更好的方法,也就是把函数当作一个处理过程 (process)。上边的函数可以转换成这样一个用自然语言描述的过程:

1. 首先检查 lst 是否为空,如果是空的,obj 当然不是这个列表的元素。处理完毕。

2. 否则,检查 obj 是否是 lst 的第一个元素。如果是的话,obj 是 lst 的元素。

3. 否则,当且只当 obj 是 lst 剩余部分的一个元素的时候,它是 lst 的一个元素。

这样是不是很直白?使用自然语言能帮助我们理解一个递归函数。

再举个现实世界中的例子。假设现在有个党史学家,他想研究一下党员数量的变化。他研究的过程可能是这样的:

1. 找一份资料。

2. 寻找跟党员数量相关的信息

3. 如果这份资料提到了其他可能有用的资料,研究这些资料。

显然这是个递归的过程。对一个过程来说,递归是很自然的。现实世界中有各种各样的递归过程。所以把函数想成一个过程,而不是一个机器,会帮我们更好的理解递归。

再回到上边的 member 函数,不要把它想成一个用于判断对象是否在列表中的机器。把它当作一组进行判断的规则,顺着这组规则走下来,我们就能做出正确的判断。

(当然第二个例子是本文作者虚拟的。在现实世界中,党说党员数量是怎么变化的,党员数量就是怎么变化的。)