可视化编程的误区
为了做比较,先来看一个可视化 Python 程序的例子:Online Python Tutor
试一下它提供的“阶乘”(factorial)例子,点击 Visualize,屏幕上出现了两栏,箭头示意代码执行到了哪一行,屏幕右侧出现了很多变量和值,点了几下“前进”之后程序输出了答案,但你明白什么是递归了么?
新手不知道栈帧(Stack Frame)是怎么回事,即使是一个老手也不见得需要知道,因为它和编程没有关系,调用栈仅仅是编程语言实现“子程序调用”的方法。
再看一个 Vivid 中的阶乘。不了解 Lisp 的朋友可能会感到陌生,所以我把整个过程转化为了以下对话:
- 老师:小明,请问 4 的阶乘是多少?
- 小明:报告老师,这个问题太难了,我心算不出。
- 老师:那问你一个简单的问题,3 的阶乘什么?只要你知道 3 的阶乘,乘上 4 不就是 4 的阶乘吗?你告诉我 3 的阶乘是几啊?
- 小明:还是不知道。
- 老师:真笨呀,再问你一个更简单的问题,2 的阶乘是多少?只要你知道 2 的阶乘,也就不难知道 3 的阶乘……那么 2 的阶乘是?
- 小明:这个嘛,你让我看一眼阶乘的定义先……
- 老师:你真是气死我了,好吧,再简单一点,1 的阶乘是多少?只要你知道 1 的阶乘,乘 2 不就是 2 的阶乘吗?
- 小明:书上说 1 的阶乘等于 1。
- 老师:那么 2 的阶乘呢?
- 小明:是“1 的阶乘”乘上 2,也就是 2。
- 老师:3 的阶乘是?
- 小明:“2 的阶乘”乘 3,等于 6。
- 老师:那么……
- 小明:4 的阶乘是“3 的阶乘”乘 4,得 24。
- 老师:小明你都会抢答了!
这是一个隐喻,它漂亮地解释了什么是递归,把函数调用比喻成问问题,那么递归的过程就可以想象成:不断把一个较难问题转化为一个较容易的问题,直到问题的答案变得显而易见。
这个例子仅仅展示了递归的概念,其他编程概念(如迭代、条件控制、参数化等)同样可以用这种形式呈现。通过这个隐喻我们可以把编程与现实世界联系起来,然后理解编程,学习编程。
Polya 在《怎样解题》指出,好的老师应该对学生循循善诱,因此问答是一种很好的教学形式。当然这个想法不是我原创的,早在 1974 年 Daniel P. Friedman 就写了一本叫《The Little Lisper》的书,在这本只有 68 页的书中他就使用了这种问答的形式。这本书现在出到了第四版,名字改为了《The Little Schemer》,使用 Scheme 的一个子集作为教学语言。
可能有人会说,Vivid 只是把程序的执行转换为对话,这是可视化吗?不是。
问题在于什么是可视化?我们可以这样可视化存储器:
| 0 | 12 |
| 1 | 16 |
| 2 | 17 |
.....
可视化 C 语言中的的栈帧:
| 0xccccccc0 | 局部变量1 |
| 0xccccccc1 | 局部变量2 |
| 0xccccccc2 | 返回地址 |
.....
但即使把存储器画得再逼真,也没有任何意义。
假设我们要可视化一棵树,可以画成这样:
但其实我们只要这样:
18
17
8
9
21
或者这样:
(18 (17 (8) 9 21))
我们没有任何画图工具,只用了几个空格或括号,就表达出了树,因为这就是一棵树所包含的全部信息量:层次信息加上一些值。
虽然直观地显示数据也很重要,但不应该本末倒置。当我们把 (18 (17 (8) 9 21)) 画成一棵树时并没有增加任何信息量。真正的可视化编程应该把程序员看不到的东西显示出来。
可视化编程根本一文不值,不过是因为它可视化了错误的东西。传统的可视化环境可视化了代码和静态结构,但是我们不需要理解它们,我们需要理解代码在做什么。
而《The Little Schemer》这本书把程序做了什么(沿着某条路径求值)展现在了读者面前,没有用图片,而是用自然语言,学习者看懂对话,自然也就明白代码做了些什么。
如果说有什么美中不足的地方,就是书中的问答采用了线性排版,因此层次信息依旧是隐藏的,需要学习者记在自己的脑袋里,递归几层以后思路容易乱掉,再一深脑袋就溢出了(so deep that your head is going to pop off)。Vivid 就是想解决这个问题。
作为一种教人编程的工具最重要的就是要易于理解,《The Little Schemer》是怎么做到易于理解的呢?有两点:
一是通过大量的例子。回想一下我们学习数学的过程:从一个简单的例子入手,依葫芦画瓢,看它是怎么工作的,重复两三次以后我们就理解了。
Richard Feynman 在日本讲学时就发现他和那些日本学者最大的区别就是他们喜欢抽象思考,而他喜欢从一个实际的例子入手:
“很好,那么就举一个例子。”这是我的作风:除非我脑袋里能出现一个具体的例子,然后根据这个特例来演算下去,否则我无法理解他们说的东西。所以很多人一开始时会觉得我反应有点慢,不了解问题所在,因为我问一大堆笨问题,像“阴极是正的还是负的?阴离子是这样的还是那样的?”
……他以为我是一步步地跟着他演算,其实不然。我脑中想的,是他正在分析的理论中某个特定、实际的例子,而根据过去经验和直觉,我很清楚这例子的特性。
……所以,在日本时,除非他们给我一个实际的例子,我没有办法了解或者讨论他们的研究工作,但是大多数人都提不出这样的例子。提得出来的例子往往极为薄弱,用其他更简单的方法就可以解决问题了。
二是在相似的概念身上寻找“同构”。当一个新的、抽象的概念出现在我们面前时,就像看到了一个外星人,这时我们怎么理解它?用一个熟悉的概念进行类比,找到它们的共同点,一旦建立了 A 同构于 B 的联系,就可以用过往对 B 的一切经验和直觉来认知 A。
比如当我们要理解“组合数”这个概念时,就可以用子集的概念进行类比,而子集的概念我们已经有了,下次看到“组合数”三个字时心里想集合就行了。
关于“同构”,GEB 有非常精彩的论述。实际上,理解本质上就是一个寻找同构 * 的过程,当我们能够在陌生的概念身上找到与已知概念的“同构”时,就可以用自己的语言表达出来,也就能够理解它。
而把新老概念连接起来的桥梁正是隐喻。
可视化的意义就在于找到视觉上的同构:改变 x 的大小,圆的大小也会跟着改变,所以 x 是一个变量……但许多可视化教程找到的却是“低层次”概念和编程语言之间的同构,我们怎么能够指望一个陌生的概念(比如栈帧)帮助我们理解另一个概念(递归)呢?而《The Little Schemer》通过对话的隐喻,帮助学习者找到了编程语言和现实世界之间的同构。