编程和许愿

会许愿的人,是有梦想的人。人如果没有梦想,和咸鱼有什么分别?

圣经里记载的上帝,应该是最会许愿的“人”了——神说:“要有光”,就有了光。

1991 年,Linus 说,我想要一个可以运行在 386 机器上的 Unix 系统,于是就有了 Linux。

2005 年,Linus 说,我想要一个可以支持 Linux Kernel 开发的版本管理系统,于是就有了 Git。

2012 年,罗永浩说,我要做手机,于是就有了锤子科技和 Smartisan 系列手机。

由此可见,许愿是一种非常强大的心灵力量,熟练掌握这一技能的话,几乎可以达到成语“心想事成”所描述的那种效果。比如有个名人曾经这样许愿:“给自己设定一个小目标,比如先赚它一个亿”,然后他那年就真的挣了一个亿

当然,以上举的例子,许的愿望都是非常大的。事实上,在程序员日常编程的时候,许愿也是一个非常重要的方法。通过许愿,你可以高屋建瓴、大刀阔斧的进行抽象。通过许愿,你不再是“设定一个小目标,比如先赚它一个亿”。通过许愿,你可以直接“假设我现在已经有了一个亿”

接下来就跟大家分享一下我对编程和许愿的看法。

1 许愿初体验

许愿式编程,“Programming by wishful thinking”这个概念,我是从那本非常经典的“Structure and Interpretation of Computer Programs”书里了解到的。作者以“有理数的运算”为例,提出了这种方法:

我们想要可以对有理数进行运算。我们想要能够对它们进行加、减、乘、除四则运算,并可以判断两个有理数是否相等。

让我们从一些假设开始。假设我们已经有办法从一个分子和一个分母创造出一个有理数。还假设拿到一个有理数之后,我们有办法抽出(或称为“选出”)它的分子和分母。让我们进一步假设这个构造函数和这个选择函数都已经是现成的了:

  • (make-rat <n> <d>) 返回一个有理数,它的分子是整数 <n>,它的分母是整数 <d>
  • (numer <x>) 返回有理数 <x> 的分子。
  • (denom <x>) 返回有理数 <x> 的分母。

我们在这里使用了一种非常强大的综合策略:即“许愿式思考”。我们还没有提到一个有理数是怎么 表示 的,也还没有提到 numerdenommake-rat 这三个子程序应该如何实现。但即便如此,如果我们的确已经拥有这三个子程序,那么,我们就可以用以下关系式来进行加、减、乘、除和判断是否相等的操作了(We are using here a powerful strategy of synthesis: wishful thinking. We haven't yet said how a rational number is represented, or how the procedures numer, denom, and make-rat should be implemented. Even so, if we did have these three procedures, we could then add, subtract, multiply, divide, and test equality by using the following relations):

\begin{eqnarray*}
\frac{n_{1}}{d_{1}} + \frac{n_{2}}{d_{2}} &amp; = &amp; \frac{n_{1}d_{2} + n_{2}d_{1}}{d_{1}d_{2}}\\
\frac{n_{1}}{d_{1}} - \frac{n_{2}}{d_{2}} &amp; = &amp; \frac{n_{1}d_{2} - n_{2}d_{1}}{d_{1}d_{2}}\\
\frac{n_{1}}{d_{1}} * \frac{n_{2}}{d_{2}} &amp; = &amp; \frac{n_{1}n_{2}}{d_{1}d_{2}}\\
\frac{n_{1}/d_{1}}{n_{1}/d_{1}} &amp; = &amp; \frac{n_{1}d_{2}}{d_{1}n_{2}} \\
\frac{n_{1}}{d_{1}} = \frac{n_{2}}{d_{2}} &amp;\text{ if and only if }&amp; n_{1}d_{2} = n_{2}d_{1}\\
\end{eqnarray*}

我们可以用如下子程序来表达上面这些等式1

(define (add-rat x y)
(make-rat (+ (* (numer x) (denom y))
(* (numer y) (denom x)))
(* (denom x) (denom y))))

(define (sub-rat x y)
(make-rat (- (* (numer x) (denom y))
(* (numer y) (denom x)))
(* (denom x) (denom y))))

(define (mul-rat x y)
(make-rat (* (numer x) (numer y))
(* (denom x) (denom y))))

(define (div-rat x y)
(make-rat (* (numer x) (denom y))
(* (denom x) (numer y))))

(define (equal-rat? x y)
(= (* (numer x) (denom y))
(* (numer y) (denom x))))

现在想想,我当初看到这段文字的时候,内心非常激动,简直好像给我揭开了一个全新的广阔天地。

2 自然而然的许愿

注意上面引用自 sicp 的文字,文笔非常自然,逻辑也十分清晰——至少我是这样认为的。它从我们现有的简单知识出发(1. 有理数可表示为两个整数的比值;2. 我们知道整数的加减乘除规则),没有纠结该怎么定义有理数的构造函数(make-rat)和取分子(numer)、取分母(denom)函数,而是直接“许了个愿”,假设我们已经有了这样的函数定义(其实并没有定义,只是假设已经有了这些函数的名字而已)。然后通过这些函数的名字,再加上整数的运算规则,它说,来吧,我们开始定义有理数的加减乘除运算吧!

通过这些运算函数的定义,再回过头去看构造函数、取分子、取分母函数应该怎么定义,或者说,应该定义成什么样儿才能/就能满足我们的需要,就非常直观了。甚至,如果你是一个 TDD(Test Driven Development:测试驱动开发)的信徒的话,通过运算函数的定义,你已经写好了 make-ratnumerdenom 等函数的测试用例。

相反,如果一上来就非要把 make-ratnumerdenom 这几个函数全都完整的定义出来,那这个开发的过程就有点儿拧巴了。这几个函数本身一点儿也不有趣。(事实上如果你讨厌数学的话,加减乘除的函数也一点儿都不有趣,但就作者举的这个例子来说,这些是最有趣的部分)。

类似的让开发过程变得更顺的方法,我还在 Code Complete 这本书里见到过。在那本书里,有一个关于写程序的方法论的建议,那就是先写伪代码(PPP,Pseudocode Programming Process:伪代码编程流)。通过用英语等人类语言把一个问题、类、函数等描述清楚,然后再用程序语言将其实现,是一种非常好的抽象的方法。先用伪代码(接近英语等人类语言)在一个比较高的 level 上进行抽象的思考,把解决问题的思路整理清楚;然后用程序语言在更具体的 level 上把所有细节全部实现。具体的操作起来的时候可能需要循环迭代几次。你也可以理解为先用伪代码许愿,然后再用代码实现。

有比这个更自然的方法吗?

再说一遍,许愿,是一种非常高级的抽象方法。

2.1 比 Code Complete 里的 PPP 更自然的编程方法

我认为只有一种方法比 PPP 更自然了,那就是 Donald Knuth 提出来的 Literate Programming(文学编程)。更确切的说,我指的是在 Emacs 的 Org mode 下进行的“文学编程”。

注意很多人可能会觉得 Donald Knuth 提出来的文学编程太学究了,因此完全不实用。的确是这样,现实中你可能很少会碰到使用“文学编程”方法的人。但不能否认的是,Literate Programming 的影响是非常深远的,目前主流的编程语言都自带文档系统,比如 Java 有 Javadoc、C++/C 等有事实标准的 Doxygen、Python 有 pydoc 等等等等,都是受 Literate Programming 把代码和文档放在一起的思路启发和影响的。

但是上面这些系统都只是学到了文学编程思想的一些皮毛。文学编程思想真正的精髓在于——根据我的理解——它允许你更自由、更自然的去组织你的想法、你的思路,去解决你的问题。

以 C 语言为例,如果你用 C 语言写程序,无论如何你逃不开的一些事情包括:

  • 在主体程序开始之前你要先写一大堆 #include <stdio.h> 这样的头文件包含指令。
  • 在头文件包含指令之后,你要写出所有全局变量的定义、所有内部(static)函数的声明。

    如果你不写 static 函数的声明也可以,但你必须确保这个函数的定义出现在所有调用它的函数定义之前。

等等。而如果是 Java 的话,你需要写一大堆的 import 语句(虽然现代的 IDE 都已经可以自动处理 import 了)。

这些杂事儿,你可以认为它们是计算机编程语言的不可避免之痛。但它们带来的一个问题就是,你无法很好的抽象了。到目前为止,计算机是无法抽象的思考问题的。只有人才能抽象的思考。而 C 语言等所有计算机编程语言,都是写出来让计算机去编译、执行的,用编程语言去抽象思考,是不方便的。人要抽象思考的时候,最好的方法还是用像英语、汉语这样的自然语言。

所以编程在很大程度上是把抽象的自然语言思考翻译成具体的编程语言实现的过程。注意除了一些非常简单的情况之外,这个翻译的过程很少有一气呵成的,而是抽象与具体存在交替进行的一个过程。Org mode 文学编程能把这种交替带来的思路中断的负面影响,帮你降到最低。

以 static 函数为例,在传统 C 语言编程里,你抽象的思考中意识到你需要一个 function_a,你在实现它的时候,需要中断自己的思路去记得做两件事儿:

  1. 把输入光标移到内部函数声明的位置,写下 function_a 的声明 
  2. 把输入光标移回到原来的位置

取决于你的编辑器和你的编程习惯,这两个操作难易程度、对你思路中断的负面影响,会有很大的波动幅度。另外还跟你的 C 程序长度有关系。在一个 1 万行的程序文件里跳来跳去找正确的编辑位置,对思路的打断肯定比一个只有 100 行的程序要更厉害一些。

如果是用 Org mode 的文学编程的话呢?你可以把你的思路和你的实现写在一起!像流水线一样写下来,在两种思考模式之间任意的切换。在最后的最后,你通过文学编程的 Web(CWeb、NoWeb)工具,把所有的函数整合在一起(就像编一张网一样,我觉得这个可能是 Web 这个名字的由来)。

举个例子,通过 Org mode 文学编程,我可以这样写程序:

* 我今天要写个什么什么程序

** 这需要子程序 A,它会帮我实现什么什么功能

(注意,我决定先写子程序 A,可能是因为我觉得子程序 A 最重要,也可能是
因为我觉得先写它最自然,也可能是因为我觉得它最有趣。我高兴!高兴是第一
生产力)。

(如果是直接写 C 语言的话,这里我可能就要先写一堆 #include 之类的
语句了,我不喜欢这样)。

#+BEGIN_SRC c
static int function_a()
{
//...
function_b
();
}

#+END_SRC

** 要实现 function_a,我好像还需要一个子程序 B,它会帮我实现什么什么功能

#+BEGIN_SRC c
static int function_b()
{
//...
}
#+END_SRC

* 最后的最后

#+BEGIN_SRC c

<<function_b>>
<<function_a>>
<<main>>

#+END_SRC

基本上就是这样。最后我的 main 函数就是简单的调用一下 function_a

#+BEGIN_SRC c
int main()
{
function_a
();
}

#+END_SRC

EOF.

你会发现最后我在用 NoWeb 的 <<>> 表示符号进行引用的时候,我把 function_b 排在了 function_a 前面,最后生成的代码,自然而然的就满足了 C 语言对函数定义必须出现在其被调用的位置之前的要求。

毫无疑问,用文学编程你会变得更啰嗦,但是,它会使你的表达变得更自然,让你更容易沉浸在来回使用自然语言+编程语言解决你的问题的心流(Flow)里。难怪 Knuth 老爷子这么热衷于 Literate Programming。因为它允许程序员用从心理学上讲正确的顺序(psychologically correct order)去探索一个编程问题。根本不需要纠结是应该自顶向下,还是自下而上,还是两者结合。

(说实话,其实我对 Literate Programming 完全是一知半解。只是有按照自己的理解,用过这种技巧从头实现了锤子科技的 CM 系统。)

3 许愿和 GTD

GTD 是 Get Things Done 的意思。一种非常有效的提升自己的执行力的方法,很简单,就是列一个待办事项清单,然后一项一项的干掉,每干掉一件,就划掉一项。

我在《Coders at Work》这本书里看到 jwz 接受采访被问到自己的编程方法时,他就是这么回答的:“我就列一个单子,然后一项项的划掉”。事实上我刚刚又打开了那本电子书看了看,jwz 多次提到了 list、todo-list,都是 GTD 里最重要的术语。

在我看来,GTD 和许愿其实就是一回事儿。许愿,就是许一个愿望然后去实现它。GTD,就是记下来自己要做哪些事儿,然后把它们都做掉。

所以我就接着讲一些 GTD 相关的领悟了。

GTD 的第一步,就是把要办的事情记下来。要不然的话,脑子里装不下太多东西,有很多事情就会忘记。并且因为发现自己忘了什么好像还蛮重要的事情,会让自己进入非常不良的状态,压力山大,一筹莫展,进一步降低自己的效率——有时候甚至感觉寸步难行,焦头烂额。许愿也是这样,许过的愿又忘记了的话,相当于没有愿望,没有愿望就是没有梦想——那和咸鱼还有什么分别?

记下来的另一个原因,是为了可以整理头绪。如果把所有东西都装在脑子里,千头万绪,根本不知道从哪里干起。一旦全列出来之后呢,头绪一点儿也没变少,但因为它们不再占用你的大脑(也就是你的 CPU),你甚至可以随便挑一个头开始干起来,不至于“天狗吃月亮,不知从哪儿下嘴”。没错,拖延症都一不小心被治好了。

GTD 的第二步、第三步,不好意思,我也还没练好,没有太多经验可以分享。书上说主要就是要分优先级、要清理 todo list,有些事情列出来了但发现办不到的要放弃,每周、每月都要 review,等等等等。我现在基本只做到了自己列过的单子,没什么特殊情况的话,一般都会实现掉(向老罗学习:吹过的牛逼要实现);当然,该放弃的时候,也会毫不犹豫的放弃掉(再次向老罗学习:不要怕打脸)。

最后,还是要推荐 Emacs 的 Org Mode,非常完美的支持 GTD!我在用 Literate Programming,同时用 GTD,噢噢噢(有了快感就要叫出来)!在安卓手机上还有 MobileOrg,我可以随时随地把自己的想法记下来,然后回到 Emacs 下去实现它。我收到的邮件,如果我后续想再处理的话,我会点一下“Flag”标签,这样标记过的邮件会自动进入我的 GTD 列表里。有人在公司的 Bug 系统里给我提单子的时候,这个单子也会自动进入到我的 GTD 里。同事在公司的 Gerrit 上给我提 Code Review,最后也会自动进入我的 GTD 列表里!我一天到晚什么也不干,就只要盯牢我的 GTD 列表就好了!跟炒股的大妈们一样,只要盯着大盘的行情就好了…

祝你也能早日掌握这些方法。

(上面提到的所有方法,Wishful Thinking、Literate Programming、GTD,都是需要练习的。不断的重复练习,慢慢地提高

Footnotes:

1

 这些函数是用一种名为 lisp 的语言写的。