解谜计算机科学
前言:http://www.yinwang.org/blog-cn/2018/03/21/csbook-preface
第一章:http://www.yinwang.org/blog-cn/2018/04/13/csbook
(本书版权归王垠所有,禁止转载。请认准 yinwang.org 为唯一的阅读地址,以获得最近更新。)
前言
计算机科学直到今天仍然是一个谜。它简单而美丽的精华,被压在沉重的历史包袱和功利诱惑之下。纷繁复杂的 IT 技术充斥着各种浮夸和忽悠,变成一本本大部头“圣经”,让人不知所措,头脑发涨,让外行尤其是女性望而却步。她们说,学计算机能赚钱,可是计算机知识淘汰速度太快,需要不断学习才能跟上,计算机工作枯燥,伤眼睛,伤皮肤,老得快!去看看各位计算机界前辈的照片,你会发现她们说的好像是对的 :)
“IT 男”和“极客”的苦逼名声,来源于这个领域创造者们的自大和虚伪。他们认为自己是天才,能够理解复杂的理论,所以他们喜欢把简单的问题搞复杂,然后告诉你“只有天才才能理解这种简单”。最后这种自大蔓延到整个领域。计算机科学虽然名字叫“科学”,但它的从业人员在很大程度上是宗教化的。不同信仰的教徒们盲目轻信,跟风拍马,互相鄙视,甚至掀起圣战。进入这个领域面临的,不仅是高度近视,不修边幅的同事,而且很多还很自大,喜欢显示自己聪明,觉得自己了不起。他们所谓的“聪明”,也就是能折腾那些纷繁复杂的理论和代码。发表论文来显示自己解决了一个问题,但别人看了仍然摸不着头脑。这种不健康的心理,进入了计算机科学的基因,完全违背了科学的精神。
在这种情况下产生出来的所谓“知识”,当然是淘汰速度很快的,因为他们只给了你浮于表面的东西。IT 业的很多知识就像妈妈告诉你,我把勺子放在这个抽屉里了,你记住啊!第二天她又把勺子换了一个地方,要你重新“学习”。这叫知识吗?他们把精华的东西牢牢地锁了起来,只把衍生出来的副产品给其他人。拿到这些副产品的人一知半解,又在上面加上一些乱七八糟的东西,然后转手倒卖给更下一层的学生。这样几层转手之后,你拿到的东西就只能凑合用了,不能用于产生新的想法,甚至使用中有问题还不能解决。这就是为什么很多码工折腾来折腾去,代码也只是碰巧能工作而已。没有理解原理,就成为了“知识”的奴隶。看不准方向,在错误的道路上越走越远。
爱因斯坦说:“如果你不能把一个问题跟六岁小孩解释清楚,那你并不真的理解它。” 这句话打了计算机前辈们的耳光。计算机界至今没有出现一本像物理界的『费曼讲义』那样负责任的教材。没有人从日常生活解释清楚那些基本的理论和技术是怎么回事。一方面是因为很多人并不真懂,只会照本宣科,拿别人的代码来拼凑折腾。另外一方面,很多懂了的人为了自己的私利,想掩盖这些简单的精华,故意把事情搞复杂。
我写这本书,就是为了弥补计算机业界这一空缺,改变行业的现状。它将吸引新鲜干净的血液进入这个行业,并且赋予他们力量。它也可以刷新内行人员的头脑,让他们重新理解和审视已有的知识。这样也许我们能冲破这个行业的重重迷雾,让它变得诚实,获得科学的精神,成为像物理一样踏实的学科。
很多计算机书籍都喜欢从“数学基础”开始,一开头就是长篇累牍的数学公式,定理,证明…… 结果读者还没读完数学基础就倒下睡着了,再也不想打开这本书。所以我不从数学基础开始,而是从最简单的生活常识。在认识发展的过程中,你会自己去创造出所需要的那些数学。
这本书不要求读者理解高等数学,而只需要幼稚园或者学前班水平的数学:掰手指头算加法,手算多位数加减法。它不要求,也不会试图教会你中学几何,高等数学或者物理学,你不需要那些来理解计算机科学。它不灌输给你死知识,而是从日常生活的经验出发,引导你去“重新发明”它们。最后你不是学会了知识,而是自己创造了它们。只有这样的知识才是属于自己的,才是可靠安心的,不受别人控制,也不会忘记。我讨厌“学习”这个词,因为它基本代表着死记硬背。我不是在教你,你也不是在学习,因为你自己发明了这一切。
如果有一天,一场灾难毁灭了世界上所有的计算机和电子产品,以及它们的设计文档,我希望看过这本书的人,能够根据他们的日常经验,重新创造出这些东西。对的,这本书不仅是关于编程和软件,它还会告诉你硬件是怎么回事,并且把软件和硬件统一起来。它不只是教会你一种程序语言,而是教会你所有的程序语言,它告诉你如何发明一种语言。它不要你去“记住”计算机里面有哪些东西,而是让你自己发现“需要”它们:晶体管,寄存器,指令,堆栈…… 你会发现虚拟机(VM)到底是什么,指令系统为什么那个样子,怎么创造它们…… 所有这一切,都以掰手指头的幼稚园算术为基础。
实际上,计算机科学和逻辑学是统一的,你会不知不觉理解很多看似高深的逻辑学,你会看透白胡子逻辑学家爷爷们的把戏。这种理解会为你提供更好的理解数学的工具,所以这本书不仅会帮助你理解计算机科学,而且会帮助你更好的理解数学。理解了数学你就能更好的理解物理。理解了物理,你就能更好的理解所有的科学……
有人可能怀疑这么浩大的工程,要什么时候才能完成。不会很久的,因为计算机科学最精华的部分,真的没有很多,我掰着手指头都数的出来。剩下来的都是衍生出来的技术,外加自欺欺人和商业炒作。你会掌握精华,识破忽悠和炒作,你可以衍生出自己的技术。即使你决定不进入这个领域,你也会成为一个火眼金睛的投资人,管理者,或者消费者。你不再能被这些“内行”欺骗。为了达到这个目标,你不需要损害自己的视力或者健康,不需要长出鸡窝一样的胡子,不需要成为一个对异性具有排斥力的呆子 ;)
这本书不是固定不变的,它会不断地完善和发展。有人看我写的东西就是我最大的动力,所以为了使我自己有动力写书,我会采用“快速迭代”的方法。我是一个很懒的人,我不会等书完全写完才发布它,那样我会打瞌睡以至于不能继续,所以我会分章节发布书的内容。每一章发布之后,还会经过成百上千次的修改。每一章的内容,我会在它“基本可读”之后就进行发布,而不会等到它完美。之后我会反复的思考和修改,接受人们的反馈。
这种做法对早期的读者有益,也有一定的弊端。弊端就在于,由于这些内容随时可能变化和改进,所以早期读者有时候会遇到看不懂的地方,必须之后再次阅读,才能跟上改进的思路。不过这样做也有好处,读者不用等上一年就能读到这本书,而且能跟着我的写作思路去思考,反复琢磨。俗话说,书读百遍,其义自见。他们可以跟我讨论,给我反馈,向我提问。这些都是后期读到完善作品的人无法体验到的。在某种程度上,这些人对问题的理解会更加深刻一些,因为他们被迫去进行独立思考。另外,早早的有了读者,会让我很开心,满怀着爱去做这件事。
人的短期记忆只能记住七个东西,所以我会努力让这本书简短。每个知识点都不应该长篇大论之后才能理解,而应该是正中要害。当然这篇前言也应该简短,所以前言就到此结束了。
计算机的世界,就将被你一个人重新发明出来。它的内容没有很多,真的没有很多……
第一章 - 初识计算
要完全掌握一个学科的精髓,不能从细枝末节开始。一位哲人说过,人脑的能力受限于他自己的信念,当他不相信自己的时候,他就做不到本来可以的事情。人的信心是很重要的,它却容易被挫败。如果人看到很多细节却看不到全景,他会失去信心,会觉得要到猴年马月才能掌握一个学科。所以这本书不从细节开始,它也不会给你一个笼统的学科框架,告诉你这个学科包括这么多的“分支”,每个分支又包括这么多的“子分支”,它们多么博大精深,每个分支都能出很多的专家学者……
我不想那样吓唬你。也许其它学科是博大精深的,但计算机科学… 大概不是 ;) 你可以在比较短的时间内抓住这个学科的精华,它可以整个装进你的脑子里。建立信心的一个有效方式,就是让自己相信我已经掌握了整个学科,只不过我掌握的都是精华的原理。这些原理会指引我无往不胜,理解所有我遇到过或者没遇到过的情况,看透所谓“新技术”背后的玄机。
所以我现在就告诉你这背后的原理。我会把计算机科学的基本概念和“子领域”的研究内容,用浅显的例子跟你解释。你会从一种微妙的方式体会到它们的本质。稍后我会把这些概念稍作发展,你就得到逐渐完整的把握。你一开始就掌握着整个学科,你会一直掌握着它,只不过逐渐增加更多的细节而已。这就像画画一样,先勾勒出一个轮廓,一遍遍的改进加深自己的理解。这样你就不会失去对大局的把握,被庞大复杂的印象吓倒。
很多计算机专业的学生,虽然学了那么多的课程,可是直到毕业都没能回答一个最基础的问题:什么是计算?这一章会引导你去发现这个问题的答案。不要小看这个基础问题,它是解决很多问题的关键。世界上有太多不理解它的人,他们走了很多的弯路,掉进很多的坑,制造出过度复杂或者有毛病的理论和技术。正是他们让这个学科复杂不堪,让人望而却步。
接下来,我们就来理解几个关键的概念,由此接触到计算的本质。
手指算术
每个人都做过计算,只是大部分人都没有理解自己在做什么。回想一下幼儿园(大概四岁)的时候,妈妈问你:“帮我算一下,4+3 等于几?” 你掰了一会手指,回答:7。当你掰手指的时候,你自己就是一台简单的计算机。
不要小看了这手指算术,它蕴含着深刻的原理。计算机科学植根于这类非常简单的过程,而不是复杂的高等数学。现在我们来回忆一下这个过程。
- 当妈妈问你“4+3 等于几”的时候,她是一个程序员,你是一台计算机。你得到程序员的输入:4,+,3。
- 听到妈妈的问题之后,你拿出两只手,左手伸出四个指头,右手伸出三个指头。
- 接着你开始自己的计算过程。一根根地数那些竖起来的手指,每数一根你就把它弯下去,表示它已经被数过了。你念道:“1,2,3,4,5,6,7。”
- 现在已经没有手指伸着,所以你把最后数到的那个数作为答案:7!整个计算过程就结束了。
这里应该有一段动画,但现阶段还没有。我建议你自己演练一下以上的手指计算过程。
符号和模型
这里的幼儿园手指算术包含着深刻的哲学问题,现在我们来初步体会一下这个问题。
当妈妈说“帮我算 4+3”的时候,4,+,3,三个字符传到你耳朵里,它们都是符号(symbol)。你可以把符号想象成一种表面特征:光是盯着“4”和“3”这两个阿拉伯数字的曲线,一个像旗子,一个像耳朵,你是不能做什么的。你需要先用脑子把它们转换成可以操作的“模型”(model)。这就是为什么你伸出两只手,一只手表示 4,另一只表示 3。
这两只手的手势是“可操作”的。你把左手再多弯曲一个手指,它就变成“3”。你再伸开一根手指,它就变成“5”。所以手指是一个相当好的模型,它是可以动,可操作的。把符号“4”和“3”转换成手指模型之后,你就可以开始计算了。
你怎么知道“4”和“3”对应什么样的手指模型呢?因为妈妈以前教过你。十根手指,对应着 1 到 10 十个数。这就是为什么人都用十进制数做算术。
我们现在没必要深究这个问题。我只是提示你,分清“符号”和“模型”是重要的。计算机科学和逻辑学里的很多错误,过度复杂的理论和代码,都是因为没有分清符号和模型。你现在可以不去深究这个问题,但总有一天你需要理解它。
计算图
在计算机领域,我们经常用一些抽象的图示来表达计算的过程,这样就能直观地看到信息的流动和转换。这种图示看起来是一些形状用箭头连接起来。我在这里把它叫做“计算图”。
对于以上的手指算术 4 + 3
,我们可以用下图来表示它:
图中的箭头表示信息的流动方向。说到“流动”,你可以想象一下水的流动。首先我们看到数字 4 和 3 流进了一个圆圈,圆圈里有一个“+”号。这个圆圈就是你,一个会做手指加法的小孩。妈妈给你两个数 4 和 3,你现在把它们加起来,得到 7 作为结果。
注意圆圈的输入和输出方向是由箭头决定的,我们可以根据需要调整那些箭头的位置,只要箭头的连接关系和方向不变就行。它们不一定都是从左到右,也可能从右到左或者从上到下,但“出入关系”都一样:4 和 3 进去,结果 7 出来。比如它还可以是这样:
我们用带加号的圆圈表示一个“加法器”。顾名思义,加法器可以帮我们完成加法。在上个例子里,你就是一个加法器。我们也可以用其他装置作为加法器,比如一堆石头,一个算盘,某种电子线路…… 只要它能做加法就行。
具体要怎么做加法,就像你具体如何掰手指,很多时候我们是不关心的,我们只需要知道这个东西能做加法就行。圆圈把具体的加法操作给“抽象化”了,这个蓝色的圆圈可以代表很多种东西。抽象(abstraction)是计算机科学至关重要的思维方法,它帮助我们进行高层面的思考,而不为细节所累。
表达式
计算机科学当然不止 4 + 3 这么简单,但它的基本元素确实是如此简单。我们可以创造出很复杂的系统,然而归根结底,它们只是在按某种顺序计算像 4 + 3 这样的东西。
4 + 3 是一个很简单的表达式(expression)。你也许没听说过“表达式”这个词,但我们先不去定义它。我们先来看一个稍微复杂一些的表达式:
2 * (4 + 3)
这个表达式比 4 + 3
多了一个运算,我们把它叫做“复合表达式”。这个表达式也可以用计算图来表示:
你知道它为什么是这个样子吗?它表示的意思是,先计算 4 + 3
,然后把结果(7)传送到一个“乘法器”,跟 2 相乘,得到最后的结果。那正好就是 2 * (4 + 3)
这个表达式的含义,它的结果应该是 14。
为什么要先计算 4 + 3
呢?因为当我们看到乘法器 2 * ...
的时候,其中一个输入(2)是已知的,而另外一个输入必须通过加法器的输出得到。加法器的结果是由 4 和 3 相加得到的,所以我们必须先计算 4 + 3
,然后才能与 2 相乘。
小学的时候,你也许学过:“括号内的内容要先计算”。其实括号只是“符号层”的东西,它并不存在于计算图里面。我这里讲的“计算图”,其实才是本质的东西。数学的括号一类的东西,都只是表象,它们是符号或者叫“语法”。从某种意义上讲,计算图才是表达式的本质或者“模型”,而“2 * (4 + 3)”这串符号,只是对计算图的一种表示或者“编码”(coding)。
这里我们再次体会到了“符号”和“模型”的差别。符号是对模型的“表示”或者“编码”。我们必须从符号得到模型,才能进行操作。这种从符号到模型的转换过程,在计算机科学里叫做“语法分析”(parsing)。我们会在后面的章节理解这个过程。
我们现在来给表达式做一个初步的定义。这并不是完整的定义,但你应该试着理解这种定义的方式。稍后我们会逐渐补充这个定义,逐渐完善。
定义(表达式):表达式可以是如下几种东西。
- 数字是一个表达式。比如 1,2,4,15,……
- 表达式 + 表达式。两个表达式相加,也是表达式。
- 表达式 - 表达式。两个表达式相减,也是表达式。
- 表达式 * 表达式。两个表达式相乘,也是表达式。
- 表达式 / 表达式。两个表达式相除,也是表达式。
注意,由于我们之前讲过的符号和模型的差别,为了完全忠于我们的本质认识,这里的“表达式 + 表达式”虽然看起来是一串符号,它必须被想象成它所对应的模型。当你看到“表达式”的时候,你的脑子里应该浮现出它对应的计算图,而不是一串符号。这个计算图的画面大概是这个样子,其中左边的大方框里可以是任意两个表达式。
是不是感觉这个定义有点奇怪?因为在“表达式”的定义里,我们用到了“表达式”自己。这种定义叫做“递归定义”。所谓递归(recursion),就是在一个东西的定义里引用这个东西自己。看上去很奇怪,好像绕回去了一样。递归是一个重要的概念,我们会在将来深入理解它。
现在我们可以来验证一下,根据我们的定义,2 * (4 + 3)
确实是一个表达式:
- 首先根据第一种形式,我们知道 4 是表达式,因为它是一个数字。3 也是表达式,因为它是一个数字。
- 所以
4 + 3
是表达式,因为+
的左右都是表达式,它满足表达式定义的第二种形式。 - 所以
2 * (4 + 3)
是表达式,因为*
的左右都是表达式,它满足表达式定义的第四种形式。
并行计算
考虑这样一个表达式:
(4 + 3) * (1 + 2)
它对应一个什么样的计算图呢?大概是这样:
如果妈妈只有你一个小孩,你应该如何用手指算出它的结果呢?你大概有两种办法。
第一种办法:先算出 4+3,结果是 7。然后算出 1+2,结果是 3。然后算 7*3,结果是 21。
第二种办法:先算出 1+2,结果是 3。然后算出 4+3,结果是 7。然后算 7*3,结果是 21。
注意到没有,你要么先算 4+3,要么先算 1+2,你不能同时算 4+3 和 1+2。为什么呢?因为你只有两只手,所以算 4+3 的时候你就没法算 1+2,反之也是这样。总之,你妈妈只有你一个加法器,所以一次只能做一个加法。
现在假设你还有一个妹妹,她跟你差不多年纪,她也会手指算术。妈妈现在就多了一些办法来计算这个表达式。她可以这样做:让你算 4+3,不等你算完,马上让妹妹算 1+2。等到你们的结果(7 和 3)都出来之后,让你或者妹妹算 7*3。
发现没有,在某一段时间之内,你和妹妹同时在做加法计算。这种时间上重叠的计算,叫做并行计算(parallel computing)。
你和妹妹同时计算,得到结果的速度可能会比你一个人算更快。如果你妈妈还有其它几个孩子,计算复杂的式子就可能快很多,这就是并行计算潜在的好处。所谓“潜在”的意思是,这种好处不一定会实现。比如,如果你的妹妹做手指算数的速度比你慢很多,你做完了 4+3,只好等着她慢慢的算 1+2。这也许比你自己依次算 4+3 和 1+2 还要慢。
即使妹妹做算术跟你一样快,这里还有个问题。你和妹妹算出结果 7 和 3 之后,得把结果传递给下一个计算 7*3 的那个人(也许是你,也许是你妹妹)。这种“通信”会带来时间的延迟,叫做“通信开销”。如果你们其中一个说话慢,这比起一个人来做计算可能还要慢。
如何根据计算单元能力的不同和通信开销的差异,来最大化计算的效率,降低需要的时间,就成为了并行计算领域研究的内容。并行计算虽然看起来是一个“博大精深”的领域,可是你如果理解了我这里说的那点东西,就很容易理解其余的内容。
变量和赋值
如果你有一个复杂的表达式,比如
(5 - 3) * (4 + (2 * 3 - 5) * 6)
由于它有比较多的嵌套,人的眼睛是难以看清楚的,它要表达的意义也会难懂。这时候,你希望可以用一些“名字”来代表中间结果,这样表达式就更容易理解。
打个比方,这就像你有一个亲戚,他是你妈妈的表姐的女儿的丈夫。你不想每次都称他“我妈妈的表姐的女儿的丈夫”,所以你就用他的名字“叮当”来指代他,一下子就简单了。
我们来看一个例子。之前的复合表达式
2 * (4 + 3)
其实可以被转换为等价的,含有变量的代码:
{
a = 4 + 3 // 变量 a 得到 4+3 的值
2 * a // 代码块的值
}
其中 a
是一个名字。a = 4 + 3
是一个“赋值语句”,它的意思是:用 a 来代表 4 + 3 的值。这种名字,计算机术语叫做变量(variable)。
这段代码的意思可以简单地描述为:计算 4 + 3
,把它的结果表示为 a
,然后计算 2 * a
作为最后的结果。
有些东西可能扰乱了你的视线。两根斜杠 //
后面一直到行末的文字叫做“注释”,是给人看的说明文字。它们对代码的逻辑不产生作用,执行的时候可以忽略。许多语言都有类似这种注释,它们可以帮助阅读的人,但是会被机器忽略。
这段代码执行过程会是这样:先计算 4 + 3
得到 7,用 a
记住这个中间结果 7。接着计算 2 * a
,也就是计算 2 * 7
,所以最后结果是 14。很显然,这跟 2 * (4 + 3)
的结果是一样的。
a
叫做一个变量,它是一个符号,可以用来代表任意的值。除了 a
,你还有许多的选择,比如 b, c, d, x, y, foo, bar, u21… 只要它不会被误解成其它东西就行。
如果你觉得这里面的“神奇”成分太多,那我们现在来做更深一层的理解……
再看一遍上面的代码。这整片代码叫做一个“代码块”(block),或者叫一个“序列”(sequence)。这个代码块包括两条语句,分别是 a = 4 + 3
和 2 * a
。代码块里的语句会从上到下依次执行。所以我们先执行 a = 4 + 3
,然后执行 2 * a
。
最后一条语句 2 * a
比较特别,它是这个代码块的“值”,也就是最后结果。之前的语句都是在为生成这个最后的值做准备。换句话说,这整个代码块的值就是 2 * a
的值。不光这个例子是这样,这是一个通用的原理:代码块的最后一条语句,总是这个代码块的值。
我们在代码块的前后加上花括号 {...}
进行标注,这样里面的语句就不会跟外面的代码混在一起。这两个花括号叫做“边界符”。我们今后会经常遇到代码块,它存在于几乎所有的程序语言里,只是语法稍有不同。比如有些语言可能用括号 (...)
或者 BEGIN...END
来表示边界,而不是用花括号。
这片代码已经有点像常用的编程语言了,但我们暂时不把它具体化到某一种语言。我不想固化你的思维方式。在稍后的章节,我们会把这种抽象的表达法对应到几种常见的语言,这样一来你就能理解几乎所有的程序语言。
另外还有一点需要注意,同一个变量可以被多次赋值。它的值会随着赋值语句而改变。举个例子:
{
a = 4 + 3
b = a
a = 2 * 5
c = a
}
这段代码执行之后,b
的值是 7,而 c
的值是 10。你知道为什么吗?因为 a = 4 + 3
之后,a 的值是 7。b = a
使得 b
得到值 7。然后 a = 2 * 5
把 a
的值改变了,它现在是 10。所以 c = a
使得 c
得到 10。
对同一个变量多次赋值虽然是可以的,但通常来说这不是一种好的写法,它可能引起程序的混淆,应该尽量避免。只有当变量表示的“意义”相同的时候,你才应该对它重复赋值。
编译
一旦引入了变量,我们就可以不用复合表达式。因为你可以把任意复杂的复合表达式拆开成“单操作算术表达式”(像 4 + 3 这样的),使用一些变量记住中间结果,一步一步算下去,得到最后的结果。
举一个复杂点的例子,也就是这一节最开头的那个表达式:
(5 - 3) * (4 + (2 * 3 - 5) * 6)
它可以被转化为一串语句:
{
a = 2 * 3
b = a - 5
c = b * 6
d = 4 + c
e = 5 - 3
e * d
}
最后的表达式 e * d
,算出来就是原来的表达式的值。你观察一下,是不是每个操作都非常简单,不包含嵌套的复合表达式?你可以自己验算一下,它确实算出跟原表达式一样的结果。
在这里,我们自己动手做了“编译器”(compiler)的工作。通常来说,编译器是一种程序,它的任务是把一片代码“翻译”成另外一种等价形式。这里我们没有写编译器,可是我们自己做了编译器的工作。我们手动地把一个嵌套的复合表达式,编译成了一系列的简单算术语句。
这些语句的结果与原来的表达式完全一致。这种保留原来语义的翻译过程,叫做编译(compile)。
我们为什么需要编译呢?原因有好几种。我不想在这里做完整的解释,但从这个例子我们可以看到,编译之后我们就不再需要复杂的嵌套表达式了。我们只需要设计很简单的,只会做单操作算术的机器,就可以算出复杂的嵌套的表达式。实际上最后这段代码已经非常接近现代处理器(CPU)的汇编代码(assembly)。我们只需要多加一些转换,它就可以变成机器指令。
我们暂时不写编译器,因为你还缺少一些必要的知识。这当然也不是编译技术的所有内容,它还包含另外一些东西。但从这一开头,你就已经初步理解了编译器是什么,你只需要在将来加深这种理解。
函数
到目前为止,我们做的计算都是在已知的数字之上,而在现实的计算中我们往往有一些未知数。比如我们想要表达一个“风扇控制器”,有了它之后,风扇的转速总是当前气温的两倍。这个“当前气温”就是一个未知数。
我们的“风扇控制器”必须要有一个“输入”(input),用于得到当前的温度 t,它是一个温度传感器的读数。它还要有一个输出,就是温度的两倍。
那么我们可以用这样的方式来表达我们的风扇控制器:
t -> t*2
不要把这想成任何一种程序语言,这只是我们自己的表达法。箭头 ->
的左边表示输入,右边表示输出,够简单吧。
你可以把 t
想象成从温度传感器出来的一根电线,它连接到风扇控制器上,风扇控制器会把它的输入(t)乘以 2。这个画面像这个样子:
我们谈论风扇控制器的时候,其实不关心它的输入是哪里来的,输出到哪里去。如果我们把温度传感器和风扇从画面里拿掉,就变成这个样子:
这幅图才是你需要认真理解的函数的计算图。你发现了吗,这幅图画正好对应了之前的风扇控制器的符号表示:t -> t*2
。看到符号就想象出画面,你就得到了符号背后的模型。
像 t -> t*2
这样具有未知数作为输入的构造,我们把它叫做函数(function)。其中 t
这个符号,叫做这个函数的参数。
参数,变量和电线
你可能发现了,函数的参数和我们之前了解的“变量”是很类似的,它们都是一个符号。之前我们用了 a, b, c, d, e
现在我们有一个 t
,这些名字我们都是随便起的,只要它们不要重复就好。如果名字重复的话,可能会带来混淆和干扰。
其实参数和变量这两种概念不只是相似,它们的本质就是一样的。如果你深刻理解它们的相同本质,你的脑子就可以少记忆很多东西,而且它可能帮助你对代码做出一些有趣而有益的转化。在上一节你已经看到,我用“电线”作为比方来帮助你理解参数。你也可以用同样的方法来理解变量。
比如我们之前的变量 a
:
{
a = 4 + 3
2 * a
}
它可以被想象成什么样的画面呢?
我故意把箭头方向画成从右往左,这样它就更像上面的代码。从这个图画里,你也许可以看到变量 a
和风扇控制器图里的参数 t
,其实没有任何本质差别。它们都表示一根电线,那根电线进入乘法器,将会被乘以 2,然后输出。如果你把这些都看成是电路,那么变量 a
和参数 t
都代表一根电线而已。
然后你还发现一个现象,那就是你可以把 a
这个名字换成任何其它名字(比如 b
),而这幅图不会产生实质的改变。
这说明什么问题呢?这说明以下的代码(把 a
换成了 b)
跟之前的是等价的:
{
b = 4 + 3
2 * b
}
根据几乎一样的电线命名变化,你也可以对之前的函数得到一样的结论:t -> t*2
和 u -> u*2
,和 x -> x*2
都是一回事。
名字是很重要的东西,但它们具体叫什么,对于机器并没有实质的意义,只要它们不要相互混淆就可以。但名字对于人是很重要的,因为人脑没有机器那么精确。不好的变量和参数名会导致代码难以理解,引起程序员的混乱和错误。所以通常说来,你需要给变量和参数起好的名字。
什么样的名字好呢?我会在后面集中讲解。
有名字的函数
既然变量可以代表“值”,那么一个自然的想法,就是让变量代表函数。所以就像我们可以写
a = 4 + 3
我们似乎也应该可以写
f = t -> t*2
对的,你可以这么做。f = t->t*2
还有一个更加传统的写法,就像数学里的函数写法:
f(t) = t*2
请仔细观察 t
的位置变化。我们在函数名字的右边写一对括号,在里面放上参数的名字。
注意,你不可以只写
f = t*2
你必须明确的指出函数的参数是什么,否则你就不会明白函数定义里的 t
是什么东西。明确指出 t
是一个“输入”,你才会知道它是函数的输入,是一个未知数,而不是在函数外面定义的其它变量。
这个看似简单的道理,很多数学家都不明白,所以他们经常这样写书:
有一个函数 y = x*2
这是错误的,因为他没有明确指出“x
是函数 y 的参数”。如果这句话之前他们又定义过 x
,你就会疑惑这是不是之前那个 x
。很多人就是因为这些糊里糊涂的写法而看不懂数学书。这不怪他们,只怪数学家自己对于语言不严谨。
函数调用
有了函数,我们可以给它起名字,可是我们怎么使用它的值呢?
由于函数里面有未知数(参数),所以你必须告诉它这些未知数,它里面的代码才会执行,给你结果。比如之前的风扇控制器函数
f(t) = t*2
它需要一个温度作为输入,才会给你一个输出。于是你就这样给它一个输入:
f(2)
你把输入写在函数名字后面的括号里。那么你就会得到输出:4。也就是说 f(2)
的值是 4。
如果你没有调用一个函数,函数体是不会被执行的。因为它不知道未知数是什么,所以什么事也做不了。那么我们定义函数的时候,比如
f(t) = t*2
当看到这个定义的时候,机器应该做什么呢?它只是记录下:有这么一个函数,它的参数是 t
,它需要计算 t*2
,它的名字叫 f
。但是机器不会立即计算 t*2
,因为它不知道 t
是多少。
分支
直到现在,我们的代码都是从头到尾,闷头闷脑地执行,不问任何问题。我们缺少一种“问问题”的方法。比如,如果我想表达这样一个“食物选择器”:如果气温低于 22 度,就返回 “hotpot” 表示今天吃火锅,否则返回 “ice cream” 表示今天吃冰激凌。
我们可以把它图示如下:
中间这种判断结构叫做“分支”(branching),它一般用菱形表示。为什么叫分支呢?你想象一下,代码就像一条小溪,平时它沿着一条路线流淌。当它遇到一个棱角分明的大石头,就分成两个支流,分开流淌。
我们的判断条件 t < 22
就像一块大石头,我们的“代码流”碰到它就会分开成两支,分别做不同的事情。跟溪流不同的是,这种分支不是随机的,而是根据条件来决定,而且分支之后只有一支继续执行,而另外一边不会被执行。
我们现在看到的都是图形化表示的模型,为了书写方便,现在我们要从符号的层面来表示这个模型。我们需要一种符号表示法来表达分支,我们把它叫做 if
(如果)。我们的饮料选择器代码可以这样写:
t -> if (t < 22)
{
"hotpot"
}
else
{
"ice cream"
}
它是一个函数,输入是一个温度。if
后面的括号里放我们的判断条件。后面接着条件成立时执行的代码块,然后是一个 else
,然后是条件不成立时执行的代码。它说:如果温度低于 22 度,我们就吃火锅,否则就吃冰激凌。
其中的 else
是一个特殊的符号,它表示“否则”。看起来不知道为什么 else
要在那里?对的,它只是一个装饰品。我们已经有足够的表达力来分辨两个分支,不过有了 else
似乎更加好看一些。很多语言里面都有 else 这个标记词在那里,所以我也把它放在那里。
这只是一个最简单的例子,其实那两个代码块里面不止可以写一条语句。你可以有任意多的语句,就像这样:
t ->
if (t < 22)
{
a = 4 + 3
b = a * 2
"hotpot"
}
else
{
x = "ice cream"
x
}
这段代码和之前是等价的,你知道为什么吗?
字符串
上面一节出现了一种我们之前没见过的东西,我为了简洁而没有介绍它。这两个分支的结果,也就是加上引号的 “hotpot” 和 “ice cream”,它们并不是数字,也不是其它语言构造,而是一种跟数字处于几乎同等地位的“数据类型”,叫做字符串(string)。字符串是我们在计算机里面表示人类语言的基本数据类型。
关于字符串,在这里我不想讲述更加细节的内容,我把对它的各种操作留到以后再讲,因为虽然字符串对于应用程序很重要,它却并不是计算机科学最关键最本质的内容。
很多计算机书籍一开头就讲很多对字符串的操作,导致初学者费很大功夫去做很多打印字符串的练习,结果几个星期之后还没学到“函数”之类最根本的概念。这是非常可惜的。
布尔值
我们之前的 if
语句的条件 t < 22
其实也是一个表达式,它叫做“布尔表达式”。你可以把小于号 <
看成是跟加法一类的“操作符”。它的输入是两个数值,输出是一个“布尔值”。什么是布尔值呢?布尔值只有两个:true 和 false,也就是“真”和“假”。
举个例子,如果 t
的值是 15,那么 t < 22
是成立的,那么它的值就是 true。如果 t
的值是 23,那么 t < 22
就不成立,那么它的值就是 false。是不是很好理解呢?
我们为什么需要“布尔值”这种东西呢?因为它的存在可以简化我们的思维。对于布尔值也有一些操作,这个我也不在这一章赘述,放到以后细讲。
计算的要素
好了,现在你已经掌握了计算机科学的几乎所有基本要素。每一个编程语言都包括这些构造:
- 基础的数值。比如整数,字符串,布尔值等。
- 表达式。包括基本的算术表达式,嵌套的表达式。
- 变量和赋值语句。
- 分支语句。
- 函数和函数调用。
你也许可以感觉到,我是把这些构造按照“从小到大”的顺序排列的。这也许可以帮助你的理解。
现在你可以回想一下你对它们的印象。每当学习一种新的语言或者系统,你只需要在里面找到对应的构造,而不需要从头学习。这就是掌握所有程序语言的秘诀。这就像学开车一样,一旦你掌握了油门,刹车,换挡器,方向盘,速度表的功能和用法,你就学会了开所有的汽车,不管它是什么型号的汽车。
我们在这一章不仅理解了这些要素,而且为它们定义了一种我们自己的“语言”。显然这个语言只能在我们的头脑里运行,因为我们没有实现这个语言的系统。在后面的章节,我会逐渐的把我们这种语言映射到现有的多种语言里面,然后你就能掌握这些语言了。
但是请不要以为掌握了语言就学会了编程或者学会了计算机科学。掌握语言就像学会了各种汽车部件的工作原理。几分钟之内,初学者就能让车子移动,转弯,停止。可是完了之后你还需要学习交通规则,你需要许许多多的实战练习和经验,掌握各种复杂情况下的策略,才能成为一个合格的驾驶员。如果你想成为赛车手,那就还需要很多倍的努力。
但是请不要被我这些话吓到了,你没有那么多的竞争者。现在的情况是,世界上就没有很多合格的计算机科学驾驶员,更不要说把车开得流畅的赛车手。绝大部分的“程序员”连最基本的引擎,油门,刹车,方向盘的工作原理都不明白,思维方式就不对,所以根本没法独自上路,一上路就出车祸。很多人把过错归结在自己的车身上,以为换一辆车马上就能成为好的驾驶员。这是一种世界范围的计算机教育的失败。
在后面的章节,我会引导你成为一个合格的驾驶员,随便拿一辆车就能开好。
什么是计算
现在你掌握了计算所需要的基本元素,可是什么是计算呢?我好像仍然没有告诉你。这是一个很哲学的问题,不同的人可能会告诉你不同的结果。我试图从最广义的角度来告诉你这个问题的答案。
当你小时候用手指算 4+3
,那是计算。如果后来你学会了打算盘,你用算盘算 4+3,那也是计算。后来你从我这里学到了表达式,变量,函数,调用,分支语句…… 在每一新的构造加入的过程中,你都在了解不同的计算。
所以从最广义来讲,计算就是“机械化的信息处理”。所谓机械化,你可以用手指算,可以用算盘,可以用计算器,或者计算机。这些机器里面可以有代码,也可以没有代码,全是电子线路,甚至可以是生物活动或者化学反应。不同的机器也可以有不同的计算功能,不同的速度和性能……
有这么多种计算的事实不免让人困惑,总害怕少了点什么,其实你可以安心。如果你掌握了上一节的“计算要素”,那么你就掌握了几乎所有类型的计算系统所需要的东西。你在后面所需要做的只是加深这种理解,并且把它“对应”到现实世界遇到的各种计算机器里面。
为什么你可以相信计算机科学的精华就只有这些呢?因为计算就是处理信息,信息有它诞生的位置(输入设备,固定数值),它传输的方式(赋值,函数调用,返回值),它被查看的地方(分支)。你想不出对于信息还有什么其它的操作,所以你就很安心的相信了,这就是计算机科学这种“棋类游戏”的全部规则。
发表评论