《计算机思维》笔记
一、无情的计算
这篇文章,咱们来讲一本关于计算机的书,《计算机思维》(Computational Thinking ),作者是两位计算机科学家,美国的彼得·丹宁(Peter J. Denning)和芬兰的马蒂·泰德(Matti Tedre)。
我们要借助这本书系统地、但同时也是快速地了解一下计算机思维,我们要理解计算机是怎么回事儿,计算机能帮我们干什么,以及怎样把计算机思维用于其他领域。
为了让你印象更深,咱们先玩一个小孩爱玩的、小小的数学游戏,
- 把你出生的月份和日期这两个数字相加,得到一个数
- 把这个数乘以 18
- 把所得结果的各个数字相加
- 把上一步结果的各个数字再相加
而我能猜出来,你最后得到的那个数字是什么。
最后的数字一定是 9。这是因为,如果一个数能被 9 整除,它的各位数字相加之和就一定能被 9 整除。
这个游戏也许能让小学生感到惊奇,对大人来说就有点直白了。不过我想让你体会的是这一波操作的过程。
你有什么感受吗?把一个两位数乘以 18 可能让你感到了一点痛苦。你必须兢兢业业准确无误地按我说的步骤操作,才能得到我想要的结果。你没有自我发挥的余地。
在这个过程中,你是一个 computer。
0x1:计算师
Computer 这个词,现在我们都翻译成“计算机”。但是这个词最初的含义,可是指人。早在十七世纪就已经出现了 computer,也许应该叫做“计算者”或者“计算师”。计算师不是数学家也不是工程师,他们是专门做计算的人。
二战期间,计算师甚至还是美军一个专门的编制。当时美军发明了射程能达到好几英里的大炮,而这个大炮怎么瞄准,就涉及到很复杂的计算。要算一个瞄准角度,你至少必须考虑目标到你的距离,目标跟你的高度差,以及当时战场上的风速。
如果你学过物理,你应该知道怎么算。但是,在战场上找个懂物理的人现场拿三角函数解方程可就太慢了。计算跟“会算”是完全不同的要求。美军必须把整个计算给流程化,变成标准操作,而且还得能让好几个计算师一起算。这就要求这个计算方法满足三个条件,
- 第一,计算过程必须被拆解成很多步骤,每一步干什么必须是简单而明确的。比如这一步就做个加法、下一步就查个三角函数表,在下一步是除法,等等。
- 第二,所有这些步骤要能拆分开来,分给几个计算师,各自负责其中的一部分。而这就有个交流问题,各个计算师算出来的东西得汇总在一起,形成一个最终的结果。
- 第三,还得有个纠错机制,让计算师们能觉察到自己哪里算错了。
按今天的概念,这其实就是“分布式并行计算”。这不但是计算机方法,而且还是超级计算机的方法。所以你看,我们可不是先有了计算机之后才有的计算机思维。人们早就有了计算机思维,只不过当时没有机器可用、只能用人。
这一波流程化的操作,叫做“算法”。“算法(algorithm)”这个词,也早在十七世纪就有了。
现在已知最早的一个算法,是欧几里得在公元前300年发明的计算两个整数的“最大公约数”的方法,咱们中国人通常称之为“辗转相除法”。两个数的最大公约数,就是能同时被这两个数整除的最大的数,比如 18 和 48 的最大公约数是 6。你肯定会算最大公约数,但是你的计算方法需要自身的理解和直觉,如果你仔细想想,你也许会发现自己有点说不清是怎么算出来的。
而欧几里得提出了一个得到最大公约数的标准操作。给定任何两个数,它们的最大公约数,和这两个数的差与其中较小的那个数的最大公约数是一样的。使用这个方法,你只做减法就能得到最大公约数!比如我们用 gcd 表示最大公约数,那么,
gcd(48,18) = gcd(30,18) = gcd(18,12) = gcd(12,6) = gcd(6,6) = 6.
欧几里得可能没想到,他发明的这套操作,到现在仍然在被计算机使用。
0x2:算法的妙处
说到这里你应该看出来了,能提出算法,和自己会算,完全是两码事。算法最大的好处就是它是机械化的操作。
有了欧几里得这个算法,操作者根本就不需要知道什么叫最大公约数,他甚至不需要知道我们为什么要做这个计算,他只要会算最基本的加减乘除,能够严格地执行规则就行。这就实现了可推广。
- 想要现场解决一个数学问题,原本你需要找个专家才行。专家有多年知识储备和技巧训练,他会用到巧妙的构思,很可能还有些说不清道不明的直觉,才能给你解决。可是专家太贵了。
- 如果我们能把一类通用的问题给算法化,你就不再需要请专家了。你只要雇些计算师就行。
算法化就是标准化和流程化。这其实就是整个机械化大生产的秘密!
我们看麦当劳的食物,味道也可以,卫生也放心,价格也便宜,而你去吃的那家麦当劳,可没有著名的厨师。每个麦当劳里的食物都是一样的。任何人经过简单的培训,都能给你操作出麦当劳食品来。麦当劳有自己的研发中心,它始终在测试不同的口味和营养搭配,但是因为它会算法化,它只要定型一个,就可以推广到所有。麦当劳只恨做饭的不是机器人。
人们从十七世纪开始就在琢磨怎么让机器去做计算,而且发明了构思巧妙的计算机,只是因为没有电子管晶体管这些东西,那时候的计算机不能取代计算师。
我忍不住就想,如果人类一直没有发明电子计算机,我们今天就会有一个叫做“计算师”的职业。这很可能是一个白领工作。计算师们很可能会使用中国的算盘做各种计算,还要背诵三角函数表和对数表。他们会被分成从初级到高级的职称,会有自己的职业道德和职业规范。他们可能都穿着统一的职业装,发型一丝不苟……
但是,那将是一个非常低端的工作。做计算师不需要什么聪明才智。
而要想比他们挣得多,要想领导他们,你就得掌握计算机思维。
0x3:计算机思维
古人对计算的钻研可不仅仅是算法,哪怕是没有计算机的时代,那些最聪明的人就已经想得很深了。十七世纪尚未发生工业革命,但是启蒙运动已经开始了,牛顿已经出来了,“理性”已经在知识分子中深入人心。
就在十七世纪,笛卡尔和莱布尼茨就有一个设想,说既然计算可以变成流程化、算法化的操作,那能不能把人的推理,也给机械化呢?
今天我们都爱谈论各种心理学意义上的偏见和谬误。启蒙时代的人就已经知道,让人去推理,是非常靠不住的。人会有各种主观的判断、有很多情绪化的东西。那如果我们能把一切理论的推理都给标准化、流程化、机械化,不管是谁来操作,只要你严格遵守规则,就一定能推出客观的、正确的判断,这岂不就解决了一切争论吗?这不就是通往真理之路吗?
笛卡尔和莱布尼茨想要的,是冷酷无情的计算。
这恰恰就是计算机思维的第一原则:把人的情感和主观判断排除在计算过程之外。
好消息是我们现在已经取得了很多很多进展。后世的学者发明了像“布尔代数”、“谓词逻辑”这些理论准备,信息论祖师爷香农发明了用电路实现通用逻辑运算的方法,而且我们有了速度无法想象的快的计算机……今天真的有很多理论推导,可以交给计算机去做。计算机甚至能证明数学定理。
但是,坏消息是计算机不能解决所有的问题。现代计算机的祖师爷,图灵,用“图灵停机问题”说明,哪怕在理论上,计算机也无法通过阅读一段算法来判断这个算法到底是能自动执行到停机呢,还是会永远计算下去。我们还知道“哥德尔不完备性定理”,我们知道笛卡尔和莱布尼茨的那个梦想哪怕在数学上也是不可能实现的。
但是,我们仍然得到了一个自动化的时代!这个时代的计算机所能做的事情,一定会让十七世纪那些思想家和计算师大开眼界。
哲学家的思想总是领先于技术实现,而老百姓的思想则被技术所左右。
每个时代的技术,都会主导这个时代的人的思维。
- 老一辈的人爱说“压力很大”、“动力十足”、“把压力化为动力”,这其实是一种暗喻,是把人给当成了蒸汽时代的机车。
- 今天的年轻人则善于用计算机做暗喻,会说……“我的大脑死机了”。
计算机思维已经深入到了现代生活的方方面面。
二、自动的机器
了解一些历史有利于理清思路。上一章讲算法,我们总结了一个历史教训,那就是人的思想,可以领先于技术应用。人类并不是先有了计算机才有的计算机思维,而是早就有了计算机思维,在那儿等着计算机出现。借用一个佛教术语,启蒙时代以来思想家已经“照见”了计算机的可能性。
我们看计算机产品是最容易过时的,今年买的手机明年就换代了。硬件可以被淘汰,软件可以被升级,但是,思想很难过时,数学永不过时。计算机的发展故事,是把思想实现的故事。
我们孜孜以求的,是一个“自动的”机器。
0x1:对自动的追求
下面这张图中是一个会下国际象棋的机器人,叫“土耳其行棋傀儡(Mechanical Turk)”。它在1770年出现在欧洲,能跟人类棋手对弈,而且水平相当高。它击败过拿破仑·波拿巴和本杰明·富兰克林这样的名人。很多人怀疑棋盘下面那个柜子里可能藏了一个人,可是打开柜子只能看到一堆齿轮。这个下棋机器人在欧美纵横征战了数十年。
……当然,那时候可没有“深蓝”和“AlphaGo”。一直到 1857 年这个秘密才被揭开,事实是……里面真的藏了一个人。只不过因为设计巧妙,看不出来而已。
但是你能看出来当时的人对“自动的机器”有多么着迷。人们迫切想要这个东西,而且人们认为有这个东西很正常。“土耳其行棋傀儡”是个魔术,但下面这个东西可不是魔术,
这是“作家机械人偶(The Writer)”,由瑞士钟表师皮埃尔·雅克·德罗在1768 年设计制造。它能自己拿笔蘸墨水,在纸上写字画画,
如果你见过这个会写字的机器人,再见到会下棋的机器人可能就不会感到那么不可思议了吧。其实下棋和写字还是有本质区别,写字只是“自动”,而下棋需要智能。但是在谈论智能之前,我们得知道,自动,已经是一个了不起的成就。
这个作家机器人偶是怎么写字画画的呢?你必须能把那些字和画的信息存储到机器里才行。而这是通过机器人后背的一个齿轮实现的,齿轮上代表不同字母的钢片的形状决定了字母写出来什么样,
而那些字母是可以替换的!你换一组字母,或者换一套代表绘画笔画的齿轮,作家机器人偶就可以写出别的内容。就好像我们现在玩游戏机,换个卡带就是一个新游戏一样。
十八世纪真是个蒸汽朋克式的美好时代。如果人类一直没有发明电力,今天我们看到的就都是这样的自动机器。我们想想,像八音盒这种东西,它为什么能播放特定的乐曲?其实是一样的道理。我借用威尔逊在《生命视角》中的一个说法,这是“严格的灵活性”:操作步骤是严格固定的,但是可以接受灵活的输入,产生不同的输出。
1804 年,法国人发明了一种提花织机(Jacquard loom),它能从一个长长的打孔卡上读取信息,织出特定式样的花纹。打孔卡上的孔可以让钩子穿过去,钩子控制织线。织机是同一个织机,打孔卡可以随便更换,换张卡就是不一样花纹。
打孔卡,可以说是机械化自动机器的软件。打孔卡这个技术思想在此后有广泛的应用,IBM公司最早就是靠这东西起家的……
但是,“严格的灵活性”毕竟不是真正的灵活性。打孔卡技术,距离可编程计算,还有根本性的一步。
0x2:第一台通用计算机
你可能不知道,第一台可编程的通用计算机,并不是电子的,而是机械的。
1820年,英国人查尔斯·巴贝奇(Charles Babbage)把“严格的灵活性”发挥到了极致,设计了一个叫做“差分机”的东西,它可以做多步骤的复杂计算。
巴贝奇的野心是用差分机计算正确的数学用表。当时公开发行的对数表、三角函数表都是人手算出来的,其中有大量的错误。巴贝奇认为人是靠不住的,这种计算应该交给机器。巴贝奇跟英国政府说,数学用表的错误会导致英国海军的计算出错,那么舰艇就出事儿,搞不好可能会沉船,现在我发明了一种机器,能做冷酷无情的计算。英国政府被说服了,真给了他一笔经费。
巴贝奇的设计包括 25000 个部件,具有强大的计算能力……但是他做了十年只完成了七分之一,
而这纯粹是因为硬件技术不允许。巴贝奇必须用齿轮和杠杆实现各种运算,可是这些东西没法做得特别精细,很容易这里卡壳、那里跳线,而且零件越多越容易出问题。英国政府一直看不到成果,把巴贝奇的经费停了。
巴贝奇做不出来,可是他特别能想……1830年,巴贝奇又自费发明了一个更厉害的东西,叫“分析机”。分析机,是一个可编程的通用计算机!
分析机的神来之笔在于它把操作步骤也写进了打孔卡之中,这样计算步骤就是不固定的了,就是可编程的了。不但如此,分析机还可以根据中间的计算结果,决定下一步的计算操作,也就是说,它允许程序里有“If… then… ”这种条件语句,它甚至还包括循环语句!分析机有输入部分、有计算部分、有存储中间计算结果的部分,还有输出打印的部分……分析机,是真正的计算机。
巴贝奇还有一个合作者专门给分析机写程序,可以说是人类历史上第一个程序员,而且这是一个女程序员,叫埃达·洛夫莱斯(Ada Lovelace),他俩用了十余年的时间一直在研究分析机。洛夫莱斯甚至还想到,分析机不仅能用来做数值计算,还能作符号计算,它不仅仅是个“计算”机,它是个能处理任何信息的机器!
……可惜的是,分析机也没做成。下面这张图是分析机的一个实验部分,来看一眼世界第一台计算机吧,
人的思想要是领先时代太多,是不是一个悲剧呢?巴贝奇因为发明数学表格的计算理论工具而得过英国皇家天文学会的金奖,他还是第一个提出“科学管理”的人。但是差分机和分析机被当时的人视为是巴贝奇身上的污点,什么实用成果都没拿出来,结果有一万两千个零件被溶解报废了。
1871年巴贝奇去世的时候 ,《泰晤士报》甚至还专门讽刺了他一番。
而通用计算机这种东西再次被人提出来,则是70年后的事情了。
0x3:现代计算机
1945年,因为电子真空管技术条件成熟和美国军方资助,世界第一台能用的通用计算机终于被做出来了,这就是著名的 ENIAC 。ENIAC 是现代计算机的鼻祖,它奠定了所谓的“冯·诺依曼架构”。而在此之前,艾伦·图灵已经提出了可编程通用计算机的完整数学理论。
我认为约翰·冯·诺依曼是人类历史上最聪明的几个人之一。他是数学家、物理学家,是博弈论的创始人,还是现代计算机架构的提出者,不过冯·诺依曼自己对此是否认的,他说这个设计是一帮人讨论的结果,他只不过负责做笔记而已。
相对于巴贝奇的设计,冯·诺依曼架构的关键改进在于它有了“内存”这个概念。负责计算的CPU速度很快,而从打孔卡读取计算步骤的速度非常慢,所以现在的设计是先一次性地把打孔卡的程序信息读进来,存到内存里,然后在计算过程中让CPU直接和内存交换信息,这就能大大加快计算速度。
从提花织机的打孔卡提供了“严格的灵活性”,到巴贝奇的可编程通用计算机,到冯·诺依曼架构的CPU和内存,这是非常清晰的演变。现在我们看到了,图灵再厉害,“编程”这个思想不是他先提出的;冯·诺依曼再神,计算机系统不是他发明的。
计算机是这么一个不断改进的东西。一代一代的工程师提出各种各种巧妙的设计。
- 要在硬件上实现信息的数字化,人们发现应该用二进制。十进制很直观,但是要用硬件实现十进制,你的电子管就得用十个级别的电压代表0-9这10个数字,这不但不容易实现,而且很容易出错。二进制只需要电子管和存储点有开和关两个状态就行,这就大大增加了可靠性。
- 然后香农设计了逻辑门……从硬件到软件的关键思想是分层。
- 然后你还要考虑CPU时钟的同步问题。如果没有“步”这个概念,各方面的计算没有协调,可能会导致两组电信号同时出现,系统就会出错。
- 然后你还要考虑存储信息和操作过程中可能会有各种小错误,而这就有一个自动纠错的问题……
- 等等等等。
我就想,为什么说信息行业是个创新行业呢?
- 因为第一,这里面的想法太密集,有太多可以改进之处
- 第二,改进的方向相当明确。这就是说每个参与者都知道自己能干什么……在其他领域,可没有这么好的事情。
从计算机思维的角度来说,这一章的核心教训是你必须考虑硬件。永远都别忘了计算机是个机器,而机器有数学算法之外的、自己的特点和脾气。比如说,
x = 3
这个语句,在数学上的意思无非就是变量 x 的值是 3,是一个事实陈述。但是对编程来说,这不是一个陈述,而是一个动作:是把用 x 标记的那一段内存位置,赋值为 3。
机器不理解你要说的事实,机器只会按你的指令做各种动作。我们在编程的时候要 debug,要想办法控制内存,要提升计算性能,就必须考虑硬件自身的特点。
今天我们用的一切计算机,包括手机、汽车、凡是通用计算机,就都是冯·诺依曼架构。当然冯·诺依曼架构不是唯一的可能性。现在人们正在研究新的架构,比如量子计算机和神经网络计算机,那些东西有自身的编程特点。
不过在我看来,巴贝奇要是看到今天的计算机,是不会感到特别惊奇的。
三、全新的科学
这一章,我们要讲一个发展建设的故事,不过这里发展建设的不是一个公司也不是一个国家,而是一门新科学。我希望这一章能增加你对“学问”的亲近感。
你有没有过这样的想法:现在各个学科都要用到计算机,那为什么还要有一个单独的“计算机系”呢?这是大学在蹭一个浮夸的热点吗?是为了应对市场对程序员的巨大需求吗?不是。
计算机科学系,不是给程序员提供职业培训的地方。计算机科学系是研究计算机的地方。我们来讲讲“计算机科学”是如何发展壮大的,它为什么是一门“科学”。
不要低估天下英雄。不要以为只有像爱因斯坦的广义相对论才算了不起。计算机科学里,也有层出不穷的高妙思想……这是一个想法产生技术,技术刺激想法,想法又产生新想法的故事。
0x1:自然科学的荣誉感
从外面远处看一个很大的东西,你容易把它当成一个整体;而如果深入其中,你会发现它的各个部分之间存在严重差别。
比如外国人在中国以外谈论中国,中国是一个标准化的形象,但是咱们中国人知道,中国的各个地区发展很不平衡,中国很复杂。
大学也是这样。对一般高中生来说名校都很厉害,都是有光环的地方。而真正身处大学里面,它的各个科系其实是有区别的……这里面有个格调问题。教授们常常互相瞧不起,学科之间有个鄙视链。
当然你也可以说这是“对小区别的自恋”,但这个小区别是存在的。
在所有学科之中,自然科学能给人提供最大的荣誉感,理解自然现象,是最纯粹科研趣味。比如“快速射电暴”这个现象,老百姓对它感兴趣是因为把它当成了外星人发的信号,而在科学家看来,就算它跟外星人没关系,只要它是一个我们尚未理解的自然现象,它就是有意思的,因为这是大自然本身的东西!
反过来说,
- 如果你研究的是某款电动汽车的发动机
- 你搞了一个什么技术革新
- 你提出了一个新的网络安全架构和理论框架改革
哪怕这项革新有潜力带来巨大的利润,那也对不起,你这个入不了科学家的法眼,因为它不是一个自然现象。
- 你研究的是一个人为的东西。也许在这款电动车的架构之下有这个问题,换一个平台,可能你这个问题就根本不存在。
- 网络安全的问题本质都是人产生的,它有特定的产生和应用环境,换一个前置条件后,问题可能就不存在了。或者说,网络安全的问题本质上就是内卷博弈。
所以这个鄙视原则是:自然的 高于 人为的。
那我们想想,“计算机科学”,是自然的还是人为的呢?
1962年,普渡大学建立了美国第一个计算机科学系,紧接着斯坦福大学也建立了一个。按当今中国的实用主义价值观来说这肯定是好事儿,但是在当时,可是引发了很大的争议。其他系的教授们纷纷声讨,说“计算机科学”算什么学问,何德何能,居然要开山立派成立新系呢?
当然这个灵魂诘问的背后还有一个利益的争夺。成立新系会把其他系的学生、经费和人员编制分流出去,等于是在削弱现有的系。电子工程系的教授完全可以说,我们也可以教计算机课啊?
这就给早期的计算机科学造成了一个重大的选择压力。计算机科学系要竖旗,就不能仅仅是一个培训程序员的地方。计算机科学家必须在企业研发之外,搞出一些有学术味道的东西,得有拿得出手的发现、能提高大学的声望才行。
其实他们不用担心。事实证明,计算机科学里中有太多拿得出手的发现了。
0x2:从技术到科学
人们研究计算机的初衷,无非是想实现自动化,搞自动计算或者自动控制,解决生产力问题,计算机是一个技术。但是计算机科学一旦成型,它自身的发展就跳出了纯技术的维度。
《计算机思维》这本书列举了计算机科学的各种进步,我认为它的发展可以分为三个方面,
- 初级,是怎么更好地实现自动化,主要解决应用问题
- 中级,是研究“计算”这件事儿本身,上升到了类似于自然科学的层面
- 高级,则是把计算思维用到其他科学领域中去,是带给人认知的升级
咱们先说初级,也就是自动化。
1940年代刚刚发明电子计算机的时候,大家都使用所谓“汇编语言”,是让人说机器的语言,直接把机器指令写在打孔卡上,给计算机读取。这个做法对人的要求实在太高了,费时费力不说,还特别容易出错。那么一个自然的需求就是,能不能让人按照人容易理解的思维写程序,然后把写出来的程序自动翻译成机器语言呢?
这就是所谓高级编程语言。1957年,Fortran语言被发明了,这是一个科学计算语言,特点是特别容易写公式。1958年,LISP语言出现,这是一个极其强大的符号处理和逻辑运算语言,甚至可以用于人工智能设计。1959年人们又发明了 COBOL 语言,用于商用数据库编程。
然后要方便人们在一台计算机上操作,你就得有账号管理,得安排不同的程序同时运行,那么你就得有操作系统,你就得有系统思维。要想开发出来的软件好用、可靠、容易维护,你就得有工程思维。接下来还有了互联网,你需要网络思维和安全思维。
所有这些自然生长出来的东西都是学问。再进一步,编程“思想”也在演化。传统编程语言是线程式的,程序员思维模式是操控计算机。后来要把软件做得很大的时候,出现了“面向对象”的程序设计。这是不一样的思维模式,程序员想的不再是操控计算机这一台机器,而是“类”和“对象”:每个类和对象就好像是一个小机器一样,是这些虚拟的小机器之间在互动。
那么随着编程语言的发展,同时计算机硬件也在变得越来越快、越来越普及,基础教育界就把编程技能,看得和阅读、写作和数学一样,被认为是人人必备的第四个技能。
但这些还都是比较浅层的计算机科学。
从计算自身到“计算宇宙”
计算机科学真正成熟的标志,是“计算机”这个东西,和“计算”这个行为本身,成了研究的对象。
1967年,三个计算机科学家在《科学》杂志上发表了一篇文章,说现在计算机科学已经称得上是一门真正的科学了:就好像动物学研究动物、植物学研究植物一样,计算机科学,研究计算机。
我以前听过一个说法,说真正的程序员,都认为计算机是有生命的。你得把计算机当做一个独立存在的东西才行,它有自己的脾气,有自己的特性和性格。要想操作好计算机,你得像动物学家理解动物一样,去理解它才行。
这个感觉对外行来说可能有点奇怪。难道计算机不是人类设计制造的吗?怎么人类还不理解计算机呢?但事实就是如此,你能设计它制造它,你可不一定就完全理解它。发明围棋的人并没有完全理解围棋。也许计算机是人类释放出来的一个怪兽!
比如说,如果几个用户同时使用一台计算机,操作系统必须给他们分配计算机CPU时间,那如何分配呢?你在理论上设计得再好,不上机去测试一下就永远不知道结果会是怎么回事儿。你得把计算当成一个生物,在它身上做实验。
与此同时,“计算理论”也在发展。给你一个问题,你能不能估算一下它的难度有多大。计算理论最关心的是,随着问题复杂度上升,计算时间是线性增长呢,还是指数增长?如果是线性增长,我们认为它是一个“简单”问题;如果是指数增长,它就是一个“难的(hard)”问题。
难的问题,计算机就帮不了太多忙。你就得想点别的办法。那么给你一个复杂问题,我们怎么把它算法化,怎么拆解它?解决这个问题的信息结构应该是什么样的?什么样的问题是可计算的,有没有什么问题是不可计算的?
再比如说假设现在有了算法,我们能不能优化这个算法,来提高运算速度呢?再比如如果让你凭空发明一个编程语言,你应该怎么做呢?
这些问题似乎像是工程上的,但又带有很强烈的数学味道!其实它们本质上是数学问题。现在有很多应用数学家就在专门研究这些问题。
到1990年代,计算机科学的江湖地位就算竖立起来了。人们甚至提出,计算,是在传统的理论和实验这两条路之外,第三条科研道路。但是紧接着的发展超出了计算机科学家的预料……其他学科,开始抢占计算机科学家的地盘。
人们突然意识到,“计算”是个非常基础的逻辑,到处都是计算。生物学家说,DNA就是一个计算系统,DNA复制就是计算操作,生命本来就是一个计算现象!我们要用计算的眼光来研究生命。
物理学家也可以说,基本粒子的运动就是信息交换啊,物理定律就是计算。
甚至有人提出,整个宇宙就是一台计算机,我们很有可能是生活在一个计算机模拟之中。不过,这个目前还仅仅停留在猜想层面,其实缺乏足够的证据支持。
但是我们现在想想,如果没有计算机科学,人们再怎么想,也很难想象宇宙是一台计算机,你根本就不会有这个概念!
所以说,计算机科学绝不仅仅是解决自动化问题的学问,它还有计算机和计算理论自身的学问,它还给我们提供了一个非常不一样的观察世界的眼光。
你是不是体会到了一点学术的魅力。一门科学就好像有生命一样,它不但要发展壮大,而且要在不同的维度上发展壮大。计算机科学就好像一棵树,它不但越长越高枝繁叶茂,而且在成长的过程中这棵树本身还在脱胎换骨,你过段时间一看树的材质都和以前不一样了。然后这棵树还影响了别人的认知,让人反思到底什么叫做“树”,甚至怀疑自己和整个世界就是一棵树……
咱们还可以把计算机科学跟那些幻想小说里的魔法和修仙的世界做个类比。这门学问的发展,就如同一个人刚出道的时候被人看不起,但是他有很多奇遇,今天学个大招明天升级一项技能,然后过了几十年换了筋骨,再过几百年居然成仙升天了,同时他还带给我们一个全新的世界观……只不过,真实世界里学问的奥妙,比魔法世界那一套更复杂也更精彩。
我看修计算机这个事儿,一点都不比修仙低级。
四、工程的复杂
这一章我们要说一个特别厉害的技能,叫做“软件工程”。以我之见,软件工程,可以说是工程管理和综合治理手段的极限。我希望你能从这一章体会一下如何治理最复杂的系统。
- 可能你是一个产品经理,主导开发过一款APP。
- 可能你是个企业家,管理一个几万人的大工厂。
- 可能你是个土木工程师,设计过一座跨海大桥。
你非常厉害,咱们中国有很多这样的厉害人物。
- 中国是手机 APP 开发大国
- 中国有很多超大型企业
- 中国有全世界最长的跨海大桥
可是,为什么中国就没有属于自己的计算机操作系统呢?为啥国产芯片不行呢?
因为那些事儿,跟现代软件工程相比,还只能算是简单的事儿。
程序员、CEO、计算机科学家,如果是拍一个超级英雄电影的话,这些人都可以是前台的英雄人物。但是躲在幕后操纵世界的,则将是一位、或者几位,软件工程大师。有句话叫“在计算机科学里,软件工程这一部分,对计算机科学家来说太难了。”
不了解软件工程,你就不知道什么叫“大”,什么叫“复杂”。
0x1:小和大
编程是个非常适合自学成才的项目。很多人不是科班出身,自学编程技术,也容易找到一个程序员的职位,甚至还可以自己开发一个小软件。
但仅限于“小”软件。比如你可以自己写一个电子邮件客户端程序,或者写一个视频编辑工具。可是如果要开发一个超大型软件,其中涉及到的学问,可就不是自学所能达到的了,那是需要在重大项目的实践中去领悟和提高的。自学也许可以让你成为一个优秀的侠客,而伟大的将帅,则只能用千万士兵的鲜血铸就。
这里面的关键是一个尺度问题。大,是不一样的。
计算机刚刚出来的时候,程序员都是身上有修士气质的手艺人。编程者经常是孤独的,能说天书一样的语言,想法高深莫测,写出来的代码仿佛有一种暴力美学,他们的眼睛跟显示器一起在黑暗中闪闪发光。编程,是一项神秘的技能。
那时候的程序都是完全自由的,计算机很贵,而程序不要钱。程序员们就好像十九世纪的艺术家一样,偶尔弄个俱乐部或者小作坊,彼此欣赏。
不过这个艺术时代并没有持续多长时间,程序员们很快就陷入了极度的悲观情绪之中。因为……错误。
写代码太容易出错了!代码越写越长,出错的频率不成比例地增加。可能你今天费了很大力气好不容易运行通过了,过了几天、遇到一个没想到的情况,发现还有一个隐藏的错误。有个程序员甚至说,他意识到,也许他的余生,都要在纠正自己的错误中度过……
程序员们终于明白,他们需要工程师思维。
我们之前讲了一些计算机科学的思维,而工程师思维和科学家思维至少有三个重大区别。
- 第一,科学家是寻找事物的规律,而工程师是去设计一个东西。科学家只要觉得这个规律有意思就可以发表,而工程师得负责任。他得确保这个东西不但要有用,而且还得安全不出事,还得考虑成本,讲究可行性,让人用得上还用得起才行。
- 第二是对知识的态度。科学家面对知识,是把自己当成一个没有利益攸关的旁观者,感觉看懂了、能总结出规律就行。而工程师,则是参与者。他不能仅仅“懂”这个知识,他是要拿来用的。
- 第三是对模型的使用。科学家喜欢简化的模型,能抓住实质就行,爱因斯坦有句名言说“什么东西都要越简单越好,要简单到不能再简单为止”。而工程师必须考虑所有的细节,“魔鬼在细节中”是工程师的座右铭。
要把写程序上升到工程的高度,跟以前那种兴趣爱好式的编程可就完全不同了。更进一步,软件工程和传统的工程也不一样。
比如你要修个桥,工程过程中哪里犯个小错误,通常也就是小错误,最多也就是让大桥的质量降级。这座大桥总共有15个桥墩,其中第五个桥墩有个地方没建好,这座桥大致上还能用。但软件就不一样了,程序中的一个小错误很可能就会导致整个系统的崩溃。
这是为啥呢?因为软件不但各处的关联非常密集,而且是个“活”的东西。比如发射火箭,软件是要控制火箭做动作的!哪个动作不对,火箭立即失控。
所以软件不但是个工程,而且比传统工程难得多。那怎么应对这种复杂呢?
0x2:小思维
早期的软件开发者想出了很多工程化的办法,起到了一定的效果。比如以前都是用汇编语言,后来发明了高级编程语言,程序员就不容易出错……当然,这时候也不需要程序员个个都有修士的气质了。
最重要的一个方法,是把常用、好用的代码“封装”起来,重复使用。如果这段代码总是被用到,已经被大家测试过很多次了,证明没有毛病,那就不要再改来改去搞定制了,我们应该把它封装成一个“库函数”。库函数具有标准化的输入和输出,程序员下次再用的时候只需要照顾好输入输出,而不必关心函数内部是什么情形,这就能大大降低出错的概率和提高编程的效率。
封装这个思想可以用在软件的各个方面。数据结构、面向对象的编程、文件系统,这些都是封装和分层。这一层的编程不用考虑底下一层的逻辑。
操作系统的内核也是一个类似的智慧。操作系统把最常用的操作计算机的动作,都事先在内核中预备好,而内核经过千锤百炼,不容易出错。等到别人写应用软件的时候,用到相关的动作,就只要调用内核就行,而不必自己直接操作计算机。这就相当于把专业的事儿交给专业的人,也就不那么容易出错了。
所有这些思想都要求对软件开发有个宏观的设计,而不只是吭哧吭哧写代码。然后你还得考虑多个人一起开发一个软件的情形,比如最起码得有个版本控制之类。
到这一步,软件业才算正式成了一个行业。在上世纪五十年代,就已经有公司专门开发软件卖钱。
……可惜这些还远远不够。
软件业从一开始就不是一个做事漂亮的行业。项目总是再延期。好不容易交付了,软件卖出去之后又总是被人发现各种毛病和错误。客户不满意,可是如果真要搞什么售后服务,到现场去给人解决问题,那几乎就是不可完成的任务……而且还有黑客攻击、还有计算机病毒!
我很早以前听过一个笑话,说一个软件工程师嘲笑一个汽车工程师,说“如果汽车行业像计算机行业一样发展,现在汽车应该一毛钱一辆。”但是汽车工程师不以为然,说“可是谁会要一辆动不动就抛锚的汽车呢?”
而早期的软件公司,对此只有两个不是办法的办法。一个办法是尽量去找那些经验丰富、头脑聪明的高水平程序员……一个办法是销售软件的时候干脆附带一个免责声明:如果因为这个软件的毛病给您造成了损失,我们概不负责。
社会对计算机的美好幻想被打破了,软件行业陷入了危机。
0x3:大思维
软件工程的问题不是你每年能培养多少高水平程序员的问题,而是复杂性问题。
小软件和大软件的根本区别在于尺度。以前一个小软件只有几千行代码,现在一个大软件要有几百万行代码。以前的软件是给一个人用,现在是多个用户共同使用一个软件。更重要的一点是,以前的软件是一个人或者几个人开发的,现在则是大型团队一起开发。
计算机思想家弗瑞德里克·布鲁克斯(Fred Brooks),曾经在上世纪六十年代末率领IBM公司300人的团队开发操作系统。他做完这件事之后很有感触,为此专门写了一本书,叫《人月神话》。
布鲁克斯提出两个感慨。
- 第一个感慨是,1个人干12个月的活,绝对不是12个人在1个月内能干完的。项目用的程序员越多,平均每个人出活的速度就越慢。所以你规划项目的时候不要算什么“人月”。
- 第二个感慨是,你这个团队做出来的软件的结构,往往和你这个团队的人员组织管理结构高度相似。所以软件工程不但要管项目,还要管人。
布鲁克斯这本书出来,人们才充分认识到软件工程的难度。现代软件工程要求,软件产品必须达到下面这五个目标,称之为“DRUSS”,
- Dependable,可信赖,让顾客真能指望上你这个软件
- Reliable,得可靠,不能总出毛病
- Usable,软件是给人用的,得让人能够上手
- Safe,用的时候不能出安全事故
- Secure,它得不容易被黑客攻击才行
现代主流操作系统,包括 Windows, Mac 和 Linux,各自都有接近一亿行代码,而且大致实现了这五个方面的要求。而即便是这三个可以说是最成熟的软件系统,其中仍然还有大量的毛病。
那怎么才能获得这种大型软件工程的能力呢?我们前面说的办法都还是小软件思维,剩下的,就只有一些经验之谈,而没有什么特别系统的行动指南了。
比如说,在系统安全方面,软件开发的首要原则是默认不给用户授权。如果非要授权用户接触一个什么东西,就必须得有显性的授权;每个程序进程只能拥有最有限的授权,等等。软件工程就是由这些原则、工作中遇到的规律、前辈传下来的经验组成的。
技术进步能解决一定的问题,比如更多的分层封装,搞虚拟机,客户端和服务器,高级编程语言,交互式开发环境,可视化的控制和数据流,更好的操作系统等等……但是技术解决不了所有的问题。
1987 年的时候,布鲁克斯写了一篇文章叫《没有银弹》,又提出一个洞见:软件工程的根本问题,是人的问题。主导软件开发的这个人,必须得能够理解高度复杂的东西才行。
写程序是永远在更新的技术,软件分为很多层,会出现各种毛病,你得确保产品满足 DRUSS 五方面的要求,你得操很多的心……你得能驾驭复杂。
像这样的人才,都是绝对的帅才。这就好比带兵打仗,你不用说指挥十万人打仗,你能把十万人安全带到战场,不哗变、不闹事、都能吃饱饭就不错了。
布鲁克斯有句名言是这么说的,
“好的判断来自经验,而经验来自坏的判断。”(Good judgement comes from experience, and experience comes from bad judgement.)
正所谓一将功成万骨枯,驾驭大型软件工程的能力,只能通过大型软件工程培养出来。
我们以前讲《生命视角》的时候说过,有些创新能力难复制,因为它是长出来的。我们中国有很多软件开发者,但是我们缺少操作系统这种级别的大型软件开发积累。我们有几代程序员试炼出来的库函数吗?我们有 Windows 3.1,Windows 95 的种子吗?我们有前辈开发者总结出来的原则、规律和教训吗?我们有自己的标准和规范吗?软件每天都在更新,但软件工程的背后,是一棵经年累月长出来的大树。
五、设计的境界
上一章说的是软件工程,这一章的主题是软件设计。
说到设计,你是否曾经“发布”过什么东西呢? 你背着人,自己暗中设计和创造一个作品,各种情况考虑周全、反复检查之后,选定一个时间,拿出来给大家看。
在校学生经常做这样的事情,比如参加表演、作报告、甚至交作业也是一种发布。工作中并不是所有人都有这样的机会。如果你没发布过东西,我建议你尝试一下这个体验,哪怕是做一桌子菜请朋友吃也算。
发布,是尽最大努力,呈现一个最好的东西的行动。发布能刺激我们完善自我。
当然我们见过很多低水平的发布。很多人只是例行公事交作业而已。有时候看学校的文艺表演,有的孩子上来弹个琴,的确是有板有眼好像都做对了,可是你坐在那里就觉得差了点什么东西。差什么呢?
只会演“对”的人,关注的仅仅是他要发布的这个东西本身;而能演“好”的人,会有意识地跟观众和环境配合。
0x1:软件工程和软件设计
软件工程要克服的最大困难是怎么确保不出问题,它的主题大约是“实现”,要忠实地实现各种要求。从软件工程角度来说,开发一个软件应该按下面这个流程走,
- 确认需求
- 把要求具体化,设定各种目标参数
- 进行开工建设
- 做好之后接收和测试
- 交付给用户
想要顺利完成这个过程是很不容易的。一般来说,差不多有1/3的项目能按时完成;1/3的项目会超时、超预算完成;还有1/3的项目则干脆就完不成。其中的重大困难当然是软件这个东西实在太复杂,里面有太多的模块和接口,千头万绪难保不出问题。
而另一个问题在于,上面这个软件开发的流程,把用户给忽略了。很多项目是在把最终产品交付用户之后,你才有可能得到用户的反馈,可是那时候已经晚了。
工程师在开发软件过程中接触不到一线用户是个大问题。你不知道用户到底是怎么用你这个产品的。
我以前看过一个报道,说某个软件公司,邀请一些真实的用户到公司来当场操作他们的软件,让自己的工程师围观。工程师们一看,原来用户使用软件的方式,和他们设想的非常不一样。比如对有些操作,明明软件提供了方便的做法,但是用户不知道;有时候用户绕来绕去很麻烦才能做一个动作。工程师纷纷表示,哪怕只看十分钟真实用户使用软件的场景,都能让他们马上就想到好几个改进软件的办法。
这就引出了软件设计的思想。
软件开发是实现功能和需求,软件设计则要考虑用户想要什么。
软件设计方面基本的做法,
- 一个是做好用户调研,深入了解用户的需求
- 一个是借鉴那些成功的设计都有哪些规律
- 还有一个叫“敏捷产品管理”,要求在开发的每个环节,都让用户介入进来,根据用户的反馈快速迭代
- 这些做法都是要把用户融合到整个开发的过程中去……
但是,这还谈不上是软件设计的精髓。
0x2:设计的新思维
没参与过创作的人很难理解设计有多重要。我突发奇想要一个功能,你给我实现一个功能,很多人以为难度和技术含量都在实现上,其实不然。知道该要什么功能,非常关键。
软件设计是一个激进的思维方式转换。以前的软件工程思维,是关注于功能和实现;现在的设计思维,则要求我们要考虑到用户和产品应用的环境。软件设计和一般产品的设计还不一样。一般产品让你感觉好用、好看比较容易,可能最多考虑一些人体工程学之类。而软件,跟用户是交互式的关系。用户会在软件中做出各种行为,软件是让用户活动的地方。
软件设计的目标,是创造一个虚拟世界。
比如游戏就是一种虚拟世界。我们在游戏里能获得沉浸式的体验,就好像自己真的生活在游戏世界里一样。创造游戏世界,显然需要有美术,要有环境,要有历史背景和故事,还有人物形象、武器、魔法、战斗动作等等,这些都是设计。但这些都是浅层的。
深层的设计包括游戏规则和游戏的平衡性。游戏里也得有操作守则和玩家的社会规范,明确什么能做什么不能做,得体现价值观。为了保证可玩性,玩家争胜的策略得讲究平衡性和可持续发展。你得让各个种族和职业的实力比较均衡,不能出现一个种族独大的情况。你还需要游戏世界有一个比较稳定的社会经济。
像这样的设计需要专业的人才。我看有些游戏是专门聘请经济学家来设计游戏里的金钱系统。
但这个“虚拟世界”说的不仅仅是游戏。所有软件应用都可以说是提供了一个虚拟世界。社交网络、淘宝购物、滴滴打车,这些应用里都要有规则,有奖惩机制,有社会规范,要体现的价值观,更不用说用户界面和美术形象之类了。
哪怕是单人应用,比如使用办公软件写个文档或者做一个财务报表,也是处在一个虚拟世界之中。你给人提供的这个世界得是完备自洽的,得在逻辑上能说得通,让人感觉很真实,各种动作很流畅才行。
软件设计,可以分为六个等级。
- -1 级,是流氓软件。这种软件不但不安全而且很可能专门留了收集信息甚至攻击系统的后门,而且完成度很低,一堆bug。但是这样的软件也有很多人用,或者是用于从非正常渠道下载什么东西,或者适用于什么点击广告赚钱之类,用户一般都是被引诱来的。而用户心里也都知道这种软件是不可信的。
- 0 级,是轻蔑的满意。基本上可以用,但是大家都知道其中有很多问题,也不怎么安全。这种软件一般是为了抢占市场而匆匆推出的,消费者为了使用软件提供的功能,也就认了。
- 1 级,是软件能够满足基本的需求,比较可靠。
- 2 级,就要提出表扬了,软件能跟环境和用户使用习惯达到完美配合。比如我们去银行的自动提款机取钱,你不需要学习它怎么用,凭直觉就可以直接使用。该有的功能它都有,而且非常可靠。要做到这一点,开发者必须对用户、社会文化背景和社会规范都有一定的了解。
- 3 级,是软件能主动规避负面的结果。这就要求设计者不但要满足用户正常的需求,还得事先把各种怪异的用法、极端的情况都考虑到,确保软件在任何情况下都没有故障。如果有黑客攻击怎么办?如果有病毒怎么办?流量突然暴增怎么办?你预判将来可能会出现什么问题,留下充足的应对空间。然后一旦出问题,你还得能快速反应。这个能力已经不是一般意义上的软件工程思维,必须结合设计思维才行。
- 4 级,也就是最高级,是这个软件能给用户带来喜悦。这意味着软件超出了用户的预期,让人用上之后会迫不及待地想要把它推荐给别人。
达到第4级别的软件,超出了当前用户的认知。凡是能做到这一点的,都曾经是一个传奇。
Unix,包括后来的 Linux 操作系统,绝对是传奇。我上大学的时候第一次接触 Linux,其实我用的很有限,但真的是有认知升级的感觉。没有什么花哨的界面,很多商用软件和游戏都不能用,但是 Linux 是一个跟 Windows 完全不同的世界,更简单,更丰富,更深刻,可以说其中有“道”,我感觉用 Linux 的时候是在跟一帮比我聪明得多的人打交道,好像把我也带得变聪明了一点。我在 Linux 面前总是谦卑的。
不过好的软件不一定非得让人谦卑。苹果的系统、微软的办公系列、地图导航、Kindle阅读器、魔兽世界这个游戏,包括现在的打车软件,都给人提供了一种以前根本不存在的东西。没它的时候你觉得日子也是这么过,可是一旦有了它,你就回不去了。像这样的软件都能立即制造一批死忠用户,哪怕一开始有点小毛病也能让人接受。
什么样的人物,能给用户提供这样的感觉?这肯定不仅仅是编程的技能。软件设计不是找几个训练有素程序员来个 996 的事儿。软件设计是技术、数学、艺术、文化和哲学的融合创造。软件设计里面有“道”。
六、方程的模拟
这一章,我们来说说“计算机模拟”在传统科学和技术研究上的应用。
计算机改变了某些物理学家的生活方式。现在包括中国在内的这些老牌核武器国家之所以都不搞核试验了,就是因为他们都掌握了用计算机模拟核试验的技术。他们占据了研发手段的制高点,所以就可以占据道德制高点……只有朝鲜还在苦哈哈地做真实验。
那“计算机模拟”到底是干什么的呢?
0x1:计算的科学
老百姓用的计算机,严格地说应该叫“个人计算机”,只有一两块 CPU。根据摩尔定律,个人计算机在以很高的速度变得越来越强大,但是这种强大是有限的。
想要在现有的技术条件下获得极端的计算能力,你需要“超级计算机”。所谓超级计算机,是把几百个、甚至几万个 CPU 连在一起共同计算,它拥有巨大的内存和海量的硬盘。这个道理就如同砌墙一样。一个工人干活的速度再快也是有限的,而像砌墙这样简单的活儿,完全可以让很多个工人、在墙的不同位置,一起干,这叫做“并行计算”。并行计算有怎么科学地安排活儿、特别是各个计算进程之间怎么通信的问题,不过只要模型合适,这里面就可以避免“人月困境”:如果你有 1 万块 CPU,你就可以拥有几乎 1 万倍的计算能力。
上世纪八十年代,超级计算机技术成熟了。这就给科学家提供了一件足以改变战争模式的武器。
传统上的科学研究有两种模式,
- 一个是实验
- 一个是理论
实验科学家要搭建一个真实的科学场景,或者是前往一个真实的科学场景去做观测。你尝试各种不同的条件,收集数据,看看能不能验证某个模型或者想法。理论科学家则是对实验观测的现象和数据提出解释。你有时候要发明一个自己的模型,甚至提出一条定律,要不但能解释现有的实验,还能预测新的结果。
“理论”和“实验”,都是人的思想和真实世界之间互相印证。但是有了超级计算机之后,就出现了一个新的研究模式,也就是“计算”。
那你可能会说,以前的实验科学家处理数据,理论科学家搞理论,不也要做计算吗?那你说的这个计算有什么不同吗?这个不同就在于,这里说的计算,是用计算机“模拟”一个系统的演化。
我给你打个比方。比如我们要研究两支军队,在给定各自的武器装备的情况下,发生一次战斗,看看双方的胜负和伤亡情况会是怎样。实验科学家的做法,就是找两支这样的军队让他们现场打一仗,但是作为搞科研来说这么干就太贵了。理论科学家的做法是估算一下双方在每一时刻的火力输出和人员消耗情况,做一个数学的推导。这么做不花钱但是你只能“估算”,你只能算个总数,最多细化到双方的每一个兵种,你必须使用某种模型来近似战场的情况,然后你要求解方程。
而所谓“计算”的方法,则是打一场虚拟的战斗。你可以用计算机模拟双方的每一个士兵,让他们按照一定的原则输出火力,以一定的几率、根据对方的火力输出被打死打伤,就好像真正的战斗一样,看看作战过程和结果是什么样的。
严格地说计算和理论都是使用模型去模拟真实世界,
- 但是计算的模型相当于直接模拟,真实得多也复杂得多。
- 理论是“求解”,是间接的。
简单地说,实验费钱,理论费脑,而计算……最多也就费点电。更重要的是计算能让我们达到那些实验无法测量和理论无法求解的地方。计算绝不仅仅是让研究加速,而是把以前的不可能变成可能。
1982 年,肯尼斯·威耳逊(Kenneth G. Wilson)因为用计算方法研究物质相变的现象而获得诺贝尔物理学奖,这是“计算”的第一个重大胜利。威耳逊就此声称,计算的时代已经到来,我们将来可以用计算模拟解决大问题。那时候就人们估计,1995 年之前,我们就可以用纯计算机模拟的方法设计一架商用客机。
而事实上果然如此。1994 年,波音公司用纯计算模拟设计了 777 客机。
0x2:怎样模拟
咱们来看看怎样搞一次真正的计算模拟研究。设计飞机的关键是搞好空气动力学。以你这个飞机的形状和尺寸,在一定的空气压强和气温之下,以一定的速度飞行的时候,气流对机翼会有一个什么样的作用力?如果有湍流,飞机能不能受得了?传统上这些问题都只能用实验回答。当然你不可能贸然把飞机送上天,大家的做法都是弄一个巨大的“风洞”,把飞机放在里面,然后用强力的鼓风机把气流吹向飞机,效果就如同飞机在气流中飞。
而另一方面,其实我们早就知道气流的运动方程,叫做“纳维尔-斯托克斯方程(Navier-Stokes equations)”。这是一个微分方程,描写流体的速度、密度和压强随时间的变化。传统的理论方法根本无法在飞机附近对这个方程求解,因为飞机的形状太复杂了,而计算机模拟就可以做这件事情。
最早提出用计算机模拟的方法求解微分方程的,正是约翰·冯·诺依曼。他的办法是把时空打上小格子,用有限的差分代替无穷小的微分。
模拟飞行气流也是这样。我们不可能模拟每一个空气分子怎么运动,那个运算量是不可想象的,我们要做的是通过纳维尔-斯托克斯方程,在飞机附近的整个空间模拟“气流”的运动。首先给飞机周围的空间打上格子
注意图中距离飞机特别近的地方因为形状复杂,必须增加格点的密度;比较远的地方形状变化小,就可以少用一些格点,减少运算量。然后我们把时间也要设定成一步一步的,相当于给时间也打格。在 t=0 的初始时刻,你设定一个比较简单的初始条件,规定好每一个格点处气体的密度、速度、压强和温度等参数。
纳维尔-斯托克斯方程告诉我们每个时间和地点的气流如何根据周围气流的情况而变化。
整个计算模拟的过程,就是在每一个时刻,根据上一时刻各个格点处的物理参数,使用纳维尔-斯托克斯方程,计算下一时刻各个格点处的物理参数。
然后你就可以根据任何时刻气流的参数,计算气流对飞机的影响。使用特定的边界条件,你还可以计算飞机对身边气流的作用。
整个过程就好像飞机在气流中飞一样。实际操作中会有很多细节上的考虑,最基本的就是三个要素,
- 网格
- 运动方程
- 时间步
你肯定能想象到,网格越小,时间步越短,模拟就越精细。但是这里面充满取舍,你必须适可而止,哪怕最厉害的超级计算机也承受不了特别精细的模拟。科学家必须想各种办法减少运算量。
0x3:计算能告诉你什么
随着计算科学的发展,人们想了各种办法来提高计算效率。比如说解微分方程,如果真的老老实实地用差分代替微分去一步一步求解,不但运算量大而且误差也大。所以现在大家都是先对方程来个“傅里叶变换”,这样计算会变得非常容易。而我们早就有了标准的“快速傅里叶变换”库函数。
有人估计,从 1950 年代到 1980 年代,数学软件的计算能力大约提高了 10^12 倍,其中 10^6 倍是硬件升级带来的,剩下 10^6 倍,则是由于数值方法的进步。
计算模拟有一个问题,可以说是哲学上的。
如果实验观察到一个特别的现象,然后你用计算机模拟真的能够重现这个现象,那你能说,现在你已经“理解”了这个现象吗?恐怕未必。你未必能说清楚是谁导致了谁,是什么样的机制在背后。而模拟研究的可怕之处就在于,也许我们根本就不应该指望能说清楚。
这就好比说两支数万人的大军在一起混战,双方各有长处和弱点,打了整整一天,战斗结束时双方都伤亡惨重,其中一方以微弱优势取胜,我们假设你拥有战场的所有信息,那你能说清楚,胜利的一方是如何胜利的吗?是因为指挥官布阵高明吗?是因为炮火更强吗?是因为对方犯了哪个低级错误吗?很可能这些因素都有,也很可能这些因素都不是决定性的。
其实不用说打仗,就是看完一场足球赛,你也很难说清赢球的一方是因为什么而赢的。在我看来这是“计算思维”带给科学研究的认知升级:传统的因果、理论、学说,无法描述真正复杂的现象。计算不能让你得到什么一句话解释,但是计算能让你更接近真实情况。
七、什么叫“可计算”
“可计算性”和“图灵停机问题”是计算机科学中的两个关键问题,我认为每个想要理解计算机的人都应该有所了解。
0x1:什么叫“可计算”
我们知道,计算机思维能给人提供一个观察世界的视角,能让我们审视自己的世界观。因为有了计算机,我们现在可以问出下面这些大问题,
- 计算机能解决一切问题吗?
- 人脑是一台计算机吗?
- 我们生活的这个世界,有没有可能像《黑客帝国》电影那样,其实是一场计算机模拟呢?
我先说结论。科学家和哲学家的当前科学理解,对第 2 和第 3 个问题,还没有答案,你只能猜想。但是对第一个问题,答案则是明确的否定:计算机不能解决一切问题。
那你可能会感到有点奇怪,如果计算机不能解决一切问题,那不就是说计算机是有局限性的吗?那人脑和真实世界怎么还可能是计算机呢?其实这并不奇怪,因为人脑和真实世界也可能是有局限性的。计算机有局限性,不等于人脑就一定强于计算机,也不等于真实世界就不是计算机。
咱们先说说这里什么叫“解决问题”,也就是什么叫“可计算”。
对计算机科学家来说,所谓可计算,就是给定输入,经过计算机的有限次操作,能得到一个确定的输出。“可计算”的意思不但是“可以算”,而且是“可以算出结果来”。
咱们把问题简化一点。我们考虑这么一种问题,它的答案要么是“是”,要么是“否”,而在逻辑上不存在模棱两可的中间状态。请问,所有这样的问题,都是计算机可以解决的吗?
答案是否定的。哪怕像是“是否”这种问题,都不全是可计算的。比如说,图灵停机问题,就是不可计算的。
0x2:图灵停机问题
让计算机执行一个“寻常的”程序,比如计算 1+2+3+…… 这么一直加到比如说 10000,只要计算机的内存足够大,只要你给的最后一个数是有限的,只要你愿意等待足够长的时间,它就终有算完、停机、输出一个结果的时候。
但是有些程序,却是永远都不会停机的。比如你可以让计算机一直循环执行下面这个命令,
while(1) { print "hello"; // 在屏幕上打印“你好” }
那么你就会看到计算机不断地打印“你好”这两个字。你要是不主动杀死这个程序,它就不会停机,它陷入了“死锁”。“死锁”是程序员的噩梦,有时候写程序说不定哪里没设计好,程序一运行起来就陷入了死锁。
所以程序员就有一个美梦。有没有一个什么软件工具,能够直接看一眼我写的程序,就自动判断它会不会出现死锁呢?
这就是“图灵停机问题”。也就是说,有没有这样一个程序,它的输入是任何一段代码和这段代码的输入参数,而它的输出则是这段代码所代表的程序,在那个参数之下运行起来之后会不会自动停机,如果判断会停机,就输出“1”;如果判断不会停机、会死锁,就输出“0”。
想要做到这一点,这个判断程序必须只读、而不能去执行那段代码,因为如果执行,而那段代码又真的不停机的话,计算机就会陷入死锁,这个判断程序就不能输出“0”了。换句话说,这个判断程序得能像一个高水平程序员一样,看一眼你写的代码就知道其中有没有死锁。如果有这么一个判断程序,岂不就是一个绝佳的 debug 工具吗?
而图灵告诉你别做梦了,不存在的。
在理论上就不可能存在那样的判断程序。这是一个不可计算的问题。
为什么呢?证明非常简单,反证法。
我们假设存在这么一个判断程序,称为 Halt(p, I)。其中 p 是 Halt 要判断是否停机的代码,I 是程序 p 的输入。
也就是说,如果程序 p 运行输入值 I 的时候会停机,那么 Halt(p,I) = 1;否则,Halt(p,I) = 0.
好,那么我们构造下面这么一个新程序,称之为 Z:
Program (x)
If Halt(x, x) then
永远循环
Else 停机
End.
我解释一下。Z 的输入,是任意的一段字符串 x。x 可以是一段程序的代码,也可以是一段程序的输入。Z 的规则是,
- 如果 Halt(x,x) 是 1,则 Z 就永远循环下去,不停机
- 如果 Halt(x,x) 是 0,则 Z 就停机
那么请问,像这样的一个程序 Z,如果把它的代码自身,作为它的输入,会怎样呢?它会停机吗?
- 如果会停机,那就意味着 Halt(Z,Z) = 1,对吧?可是你带入 Z 的规则之中,这时候 Z 就应该不停机。
- 反过来说,如果 Z 不会停机,那么 Halt(Z,Z) = 0,带入规则之后 Z 又会停机!悖论!
所以 Halt 这样的程序,根本就不可能存在。证明完毕。
这个证明是否让你想起了“理发师悖论”和“罗素悖论”?
有集合 A={x|x∉A}。那么问题来了:A是否属于A?
等价于通俗说法“理发师悖论”:理发师说只给城市里不给自己理头的人理头,然后每天找他理头的人络绎不绝,有一天问题来了:理发师要不要给自己理头?
罗素悖论的关键在于定义的自身调用。
Z 就是那个“只给不给自己理发的人理发的理发师”。
这个悖论的关键点就在于,一段代码,既是可执行的程序,又可以作为输入的字符串。所以我理解,这个矛盾的根源在于,写代码和判断代码,都是能用语言描述的行为。
同样的道理也适用于网络安全里的病毒检测。
病毒本身也是一段代码,而且是可执行的代码,而程序员的目标是通过构建一个程序,来判断这段病毒代码的二值逻辑。常用的技术是动态沙箱模拟技术,也就是将目标病毒代码实际运行起来。但是一旦判断程序进入了恶意病毒的代码逻辑,就像量子力学的概率坍缩一样,其自身的状态就被改变了。而恰恰是这种被动的改变动作,给病毒的作者提供了对抗的机会,他们可以将病毒的“真实意图”隐藏在众多的“状态链路”中。
所以,总体上说,如果想要有效地检测病毒代码,那么判断程序就不能“真正”进入到病毒代码的可执行空间中,而只能在“外部”进行观察。而这就带来了新的技术挑战。
0x3:不可计算意味着什么
现在图灵给了一个例子,说明世界上有些问题是计算机无法解决的。计算机无法判断任何一段代码是否会停机。这说明哪怕所有问题都能用数字化描写,计算机也解决不了所有问题。
这有什么意义呢?
- 这意味着总有一些事情,你只能慢慢等着它发生,而不能事先知道它的结果。
- 这意味着程序员必须把程序运行一下才知道其中有没有 bug。
- 这意味着理论是有限的,必须得实践才行。
- 这还意味着,世界上有些事情,你只能看着它发生,而无法解释它为什么会发生。这是因为你甚至都无法预测它会不会停下来,又何谈什么解释呢?
- 那也就意味着,我们不可能完全理解真实世界。我们头脑里的只是世界的模型,而世界的模型不能代表真实世界。世界上总有一些事情,是你没有办法用什么简化的模型预测的,你只能看着它发生才知道结果。
- 这也就意味着,哪怕你再聪明,人生也是值得过的。总有些事儿你想不到,你总得经历才知道。