如何学习编程语言

2015/12/01 PL

如何学习编程语言

算法是计算机的“灵魂”,编程语言是塑造计算机“灵魂”的工具

计算机是如何工作的?对于普通人来说,这很神秘。让计算机具备“灵魂”,可以按照人的意志运行,甚至某天按照计算机自己的意志运行,其核心就是“算法”。

算法就是计算机的“灵魂”,而算法的实现又依赖于计算机编程语言。

计算机编程语言的发展,是随着计算机硬件的发展而发展的。硬件速度越快、体积越小、成本越低,应用到人类社会的场景就会越多,那么所需要的算法就会越复杂,也就要求计算机的编程语言越高级。

最初重达几十吨但一秒只能运算5000次的ENIAC(世界上第一台计算机),只能做非常有限的工作,如某些情况下的弹道计算。现在任何一部手机的运算能力都可以秒杀那个年代地球上所有计算机运算能力的总和。

计算机编程语言的发展历经了从低级语言到高级语言,发展的核心思想就是“让编程更容易”。越容易使用的语言,就会有越多的人使用;越多的人使用,就有越多的协作;越多的协作,就可以创造出越复杂的产品。 现代社会,一个软件动辄几十人、几百人,甚至几千人协作都成为可能,这自然就为开发更复杂软件提供了“人力基础”。这是人类社会的一种普遍现象,即越容易使用的工具,使用的人就越多,通过大量的协作,将彻底改变某个行业继而对人类社会产生影响。

计算机语言经历了三代:第一代是机器语言,第二代是汇编语言,第三代是高级语言。

第一代语言:机器语言(相当于人类社会的原始阶段)

机器语言由数字组成所有指令。这意味着,程序员无论想完成什么样的计算任务,都只能用“0”和“1”等数字来编写,长此以往,笔者大胆预测:程序员们100%会有精神问题。

机器语言通常由二进制数字串组成,对于普通人来说,机器语言过于难理解。使用机器语言,人们将无法编出复杂的程序。如下为一段典型的机器码:

0000, 0000, 000000010000代表LOAD A, 16

0000, 0001, 000000000001代表LOAD B, 1

0001, 0001, 000000010000代表STORE B,16

第二代语言:汇编语言(相当于人类社会的手工业阶段)

为了编程的方便,以及解决更加复杂的问题。程序员们开始改进机器语言,使用英文缩写助记符来表示基本的计算机操作,这些助记符构成了汇编语言的基础。 常见的汇编语言助记符(单词)有LOAD、MOVE等,这样编程就更容易,毕竟识别几百、几千个单词,要比识别几百、几千个数字轻松多了。 汇编语言相当于人类历史上的手工业社会时期,需要技术极其娴熟的工匠,但是开发效率相对较低。

汇编语言虽然能编写高效率的程序,但是学习和使用都不是件易事,并且程序很难调试。此外,汇编语言以及早期的计算机语言(BASIC、FORTRAN等)没有考虑结构化设计原则,而是使用goto语句来作为程序流程控制的主要方法。 这样做的后果是:一大堆混乱的跳转语句使得程序几乎不可能被读懂。对于那个时代的程序员,能读懂上个月自己写的代码都成为一种挑战。

汇编语言现在仍应用于工业电子编程、软件加密/解密、计算机病毒分析等领域。

第三代:高级语言(相当于人类社会的工业化阶段)

对于简单的任务,汇编语言可以胜任,但是随着计算机渗透到了工作、生活的更多方面,一些复杂任务出现了,这时汇编语言就显得力不从心(应该说是程序员使用汇编语言解决复杂问题出现了瓶颈),于是,出现了高级语言。像人们熟知的C、C++、Java等都是高级语言。

高级语言允许程序员使用接近日常英语的指令来编写程序。越是高级的语言,越接近人类的思维,使用起来就越方便。高级语言的出现,尤其是面向对象语言的出现,使得编程的门槛和难度都大大降低了,大量的人员进入到软件开发行业,为软件爆发性增长提供了充足的人力资源。

常用的编程语言

C语言

C语言诞生于1972年,称之为现代高级语言的鼻祖,由著名的贝尔实验室发明。C语言是人们追求结构化、模块化、高效率的“语言之花”。在底层编程方向,例如嵌入式、病毒开发等应用,可以替代汇编语言来开发系统程序;在高层应用开发方向,也可以开发从操作系统(UNIX/Linux/Windows都基于C语言开发)到各种应用软件。

老鸟建议 如果大学开这门课请大学生朋友一定认真学习,不要觉得老套,因为那是经典。C语言在现代社会流行程度仍然排名前三。

C++语言

作为C语言的扩展,C++是贝尔实验室于20世纪80年代推出的。C++是一种混合语言,既可以实现面向对象编程,也可以开发C语言面向过程风格的程序。

C语言让程序员第一次可以通过结构化的理念编写出易于理解的复杂程序。尽管C语言是一种伟大的计算机语言,但是程序的代码量超过3万行时,程序员就不能很好地从总体上把握和控制这个程序了。因此,在20世纪80年代初期,很多软件项目都面临着无法解决的问题而不能顺利推进。1979年,美国贝尔实验室发明了C++。C++最初的名字叫作“带类的C”,后来才改名叫C++。国内通用叫法是“C加加”,国际通用的读法是“C plus plus”。

C++语言在科学计算、操作系统、网络通信、系统开发、引擎开发中仍然被大量使用。

Java语言

Java语言由美国SUN公司发明于1995年,是目前业界应用最广泛、使用人数最多的语言,使用人数连续多年排名世界第一,可以称之为“计算机语言界的英语”。

Java广泛应用于企业级软件开发、安卓移动开发、大数据云计算等领域,几乎涉及IT所有行业。关于Java的发展历史和特性,将在后面专门介绍。

PHP语言

PHP原为Personal Home Page的缩写,现已正式更名为PHP:Hypertext Preprocessor。PHP语言一般用于Web开发领域,大量的中小型网站,甚至某些大型网站都使用PHP开发。

Object-C和Swift语言

Object-C通常写作Objective-C或者Obj-C或OC,是由C语言衍生出来的语言,继承了C语言的特性,是扩充C的面向对象编程语言。OC主要用于苹果软件的开发。

Swift是苹果公司于2014年WWDC(苹果开发者大会)发布的新开发语言,可与OC共同运行于Mac OS和iOS平台,用于搭建基于苹果平台的应用程序。

JavaScript语言

JavaScript是一种脚本语言,已经被广泛用于Web应用开发,其应用范围越来越广泛,重要性也越来越高。目前,流行的H5开发其核心就是JavaScript语言。

Python语言

Python发明于1989年,语法结构简单,易学易懂,具有丰富和强大的库。它常被昵称为胶水语言,能够把用其他语言编写的各种模块(尤其是C/C++)很轻松地连接在一起。Python广泛应用于图形处理、科学计算、Web编程、多媒体应用、引擎开发,尤其是在未来将会大热的机器学习和人工智能领域有非常大的潜力。

C#语言

C#是微软公司发布的一种面向对象的、运行于.NET Framework之上的高级程序设计语言。C#在基于Windows操作系统的应用开发这一领域正在取代C++,占据主导地位。“成也萧何败也萧何”,由于C#的微软身份,也成为其发展的阻力,导致它在其他IT领域应用较少。

Fortran语言

Fortran是世界上第一种高级语言,由IBM公司在1954年提出,主要用在需要复杂计算的科学和工程领域,现在仍然被广泛使用,尤其是工程领域。Fortran虽然适合编写科学计算方面的程序,但是不适于编写系统程序。

BASIC语言

BASIC语言虽然易学,但功能不够强大,它应用到大程序的有效性令人怀疑,已经逐步退出历史舞台。

COBOL语言

COBOL语言诞生于1959年,主要用于大量精确处理数据的商业领域中,例如金融、银行。今天,仍然有超过一半的商业软件使用COBOL编写,有将近100万人在使用COBOL编程。

Pascal语言

Pascal的名称是为了纪念17世纪法国著名哲学家和数学家Blaise Pascal而来的,它由瑞士的Niklaus Wirth教授于20世纪60年代末设计并创立。Pascal语言语法严谨,层次分明,程序易写,可读性强,是第一个结构化编程语言,但由于没有大厂商和政府的支持,因此只限于大学教育领域。

学习程序语言是每个程序员的必经之路。可是这个世界上有太多的程序语言,每一种都号称具有最新的“特性”。所以程序员的苦恼就在于总是需要学习各种稀奇古怪的语言,而且必须紧跟“潮流”,否则就怕被时代所淘汰。

作为一个程序语言的研究者,我深深的知道这种心理产生的根源。程序语言里面其实有着非常简单,永恒不变的原理。看到了它们,就可以在很短的时间之内就能学会并且开始使用任何新的语言,而不是花费很多功夫去学习一个又一个的语言。

对程序语言的各种误解

学习程序语言的人,经常会出现以下几种心理,以至于他们会觉得有学不完的东西,或者走上错误的道路。以下我把这些心理简要分析一下。

  1. 程序语言无用论。这是国内大学计算机系的教育常见的错误。教授们常常对学生灌输:“用什么程序语言不重要,重要的是算法。”而其实,程序语言却是比算法更加精髓的东西。任何算法以及它的复杂度分析,都是相对于某种计算模型,而程序语言就是描述这种计算模型的符号系统。算法必须用某种语言表述出来,通常算法设计者使用伪码,这其实是不严谨的,容易出现推理漏洞。算法设计再好,如果不懂得程序语言的原理,也不可能高效的实现。即使实现了,也可能会在模块化和可扩展性上面有很大问题。某些算法专家或者数学家写出来的程序极其幼稚,就是因为他们忽视了程序语言的重要性。

  2. 追求“新语言”。基本的哲学告诉我们,新出现的事物并不一定是“新事物”,它们有可能是历史的倒退。事实证明,新出现的语言,可能还不如早就存在的。其 实,现代语言的多少“新概念”不存在于最老的一些语言里呢?程序语言就像商品,每一家都为了拉拢程序员作广告,而它们绝大多数的设计都可能是肤浅而短命 的。如果你看不透这些东西的设计,就会被它们蒙蔽住。很多语言设计者其实并不真的懂得程序语言设计的原理,所以常常在设计中重复前人的错误。但是为了推销 自己的语言和系统,他们必须夸夸其谈,进行宗教式的宣传。

  3. “存在即是合理”。记得某人说过:“不能带来新的思维方式的语言,是没有必要存在的。”他说的是相当正确的。世界上有这么多的语言,有哪些带来了新的思维 方式呢?其实非常少。绝大部分的语言给世界带来的其实是混乱。有人可能反驳说:“你怎么能说 A 语言没必要存在?我要用的那个库 L,别的语言不支持,只能用 A。”但是注意,他说的是存在的“必要性”。如果你把存在的“事实”作为存在的“必要性”,那就逻辑错乱了。就像如果二战时我们没能打败希特勒,现在都做 了他的奴隶,然后你就说:“希特勒应该存在,因为他养活了我们。”你的逻辑显然有问题,因为如果历史走了另外一条路(即希特勒不存在),我们会过上自由幸 福的生活,所以希特勒不应该存在。对比一个东西存在与不存在的两种可能的后果,然后做出判断,这才是正确的逻辑。按照这样的推理,如果设计糟糕的 A 语言不存在,那么设计更好的 B 语言很有可能就会得到更多的支持,从而实现甚至超越 L 库的功能。

  4. 追求“新特性”。程序语言的设计者总是喜欢“发明”新的名词,喜欢炒作。普通程序员往往看不到,大部分这些“新概念”其实徒有高深而时髦的外表,却没有实 质的内涵。常常是刚学会一个语言 A,又来了另一个语言 B,说它有一个叫 XYZ 的新特性。于是你又开始学习 B,如此继续。在内行人看来,这些所谓的“新特性”绝大部分都是新瓶装老酒。很多人写论文喜欢起这样的标题:《XYZ:A Novel Method for …》。这造成了概念的爆炸,却没有实质的进步。

  5. 追求“小窍门”。很多编程书喜欢卖弄一些小窍门,教你如何让程序显得“短小”。比如它们会跟你讲 “(i++) – (++i)” 应该得到什么结果;或者追究运算符的优先级,说这样可以少打括号;要不就是告诉你“if 后面如果只有一行代码就可以不加花括号”,等等。殊不知这些小窍门,其实大部分都是程序语言设计的败笔。它们带来的不是清晰的思路,而是是逻辑的混乱和认 知的负担。比如 C 语言的 ++ 运算符,它的出现是因为 C 语言设计者们当初用的计算机内存小的可怜,而 “i++” 显然比 “i=i+1″ 少 2 个字符,所以他们觉得可以节省一些空间。现在我们再也不缺那点内存,可是 ++ 运算符带来的混乱和迷惑,却流传了下来。现在最新的一些语言,也喜欢耍这种语法上的小把戏。如果你追求这些小窍门,往往就抓不住精髓。

  6. 针对“专门领域”。很多语言没有新的东西,为了占据一方土地,就号称自己适合某种特定的任务,比如文本处理,数据库查询,WEB编程,游戏设计,并行计算。但是我们真的需要不同的语言来干这些事情吗?其实绝大部分这些事情都能用同一种通用语言来解决,或者在已有语言的基础上做很小的改动。只不过由于各种政治和商业原因,不同的语言被设计用来占领市场。就学习而言,它们其实是无关紧要的,而它们带来的“学习负担”,其实差不多掩盖了它们带来的好处。其实从一些设计良好的通用语言,你可以学会所有这些“专用语言”的精髓,而不用专门去学它们。

  7. 宗教信仰。很多人对程序语言有宗教信仰。这跟人们对操作系统有宗教信仰很类似。其实如果你了解程序语言的本质,就会发现其实完全没必要跟人争论一些事情。某个语言有缺点,应该可以直接说出来,却被很多人忌讳,因为指出缺点总是招来争论和憎恨。这原因也许在于程序语言的设计不是科学,它类似于圣经,它没法被“证伪”。没有任何实验可以一下子断定那种语言是对的,那种是错的。所以虽然你觉得自己有理,却很难让人信服。没有人会去争论哪家的汉堡更好,却有很多人争论那种语言更好。因为很多人把程序语言当成自己的神,如果你批评我的语言,你就是亵渎我的神。解决的办法也许是,不要把自己正在用的语言看得太重要。你现在认为是对的东西,也许不久就会被你认为是错的,反之亦然。

如何掌握程序语言

看到了一些常见的错误心理,那么我们来谈一下什么样的思维方式会更加容易的掌握程序语言。

  1. 专注于“精华”和“原理”。就像所有的科学一样,程序语言最精华的原理其实只有很少数几个,它们却可以被用来构造出许许多多纷繁复杂的概念。但是人们往往 忽视了简单原理的重要性,匆匆看过之后就去追求最新的,复杂的概念。他们却没有注意到,绝大部分最新的概念其实都可以用最简单的那些概念组合而成。而对基 本概念的一知半解,导致了他们看不清那些复杂概念的实质。比如这些概念里面很重要的一个就是递归。国内很多学生对递归的理解只停留于汉诺塔这样的程序,而 对递归的效率也有很大的误解,认为递归没有循环来得高效。而其实递归比循环表达能力强很多,而且效率几乎一样。有些程序比如解释器,不用递归的话基本没法完成。

  2. 实现一个程序语言。学习使用一个工具的最好的方式就是制造它,所以学习程序语言的最好方式就是实现一个程序语言。这并不需要一个完整的编译器,而只需要写 一些简单的解释器,实现最基本的功能。之后你就会发现,所有语言的新特性你都大概知道可以如何实现,而不只停留在使用者的水平。实现程序语言最迅速的方式就是使用一种像 Scheme 这样代码可以被作为数据的语言。它能让你很快的写出新的语言的解释器。我的 GitHub 里面有一些我写的解释器的例子(比如这个短小的代码实现了 Haskell 的 lazy 语义)。

几种常见风格的语言

下面我简要的说一下几种常见风格的语言以及它们的问题。

  1. 面向对象语言

事实说明,“面向对象”这整个概念基本是错误的。它的风靡是因为当初的“软件危机”(天知道是不是真的存在这危机)。 设计的初衷是让“界面”和“实现”分离,从而使得下层实现的改动不影响上层的功能。可是大部分面向对象语言的设计都遵循一个根本错误的原则:“所有的东西 都是对象(Everything is an object)。”以至于所有的函数都必须放在所谓的“对象”里面,而不能直接被作为参数或者变量传递。这导致很多时候需要使用繁琐的设计模式 (design patterns) 来达到甚至对于 C 语言都直接了当的事情。而其实“界面”和“实现”的分离,并不需要把所有函数都放进对象里。另外的一些概念,比如继承,重载,其实带来的问题比它们解决的 还要多。

“面向对象方法”的过度使用,已经开始引起对整个业界的负面作用。很多公司里的程序员喜欢生搬硬套一些不必要的设计模式,其实什么好事情也没干,只是使得程序冗长难懂。

那 么如何看待具备高阶函数的面向对象语言,比如 Python, JavaScript, Ruby, Scala? 当然有了高阶函数,你可以直截了当的表示很多东西,而不需要使用设计模式。但是由于设计模式思想的流毒,一些程序员居然在这些不需要设计模式的语言里也采用繁琐的设计模式,让人哭笑不得。所以在学习的时候,最好不要用这些语言,以免受到不必要的干扰。到时候必要的时候再回来使用它们,就可以取其精华,去其糟粕。

  1. 低级过程式语言

那么是否 C 这样的“低级语言”就会好一些呢?其实也不是。很多人推崇 C,因为它可以让人接近“底层”,也就是接近机器的表示,这样就意味着它速度快。这里其实有三个问题:

1) 接近“底层”是否是好事?

2)“速度快的语言”是什么意思?

3) 接近底层的语言是否一定速度快?

对于第一个问题,答案是否定的。其实编程最重要的思想是高层的语义(semantics)。语义构成了人关心的问题以及解决它们的算法。而具体的实现 (implementation),比如一个整数用几个字节表示,虽然还是重要,但却不是至关重要的。如果把实现作为学习的主要目标,就本末倒置了。因为 实现是可以改变的,而它们所表达的本质却不会变。所以很多人发现自己学会的东西,过不了多久就“过时”了。那就是因为他们学习的不是本质,而只是具体的实 现。

其次,谈语言的“速度”,其实是一句空话。语言只负责描述一个程序,而程序运行的速度,其实绝大部分不取决于语言。它主要取决于 1)算法 和 2)编译器的质量。编译器和语言基本是两码事。同一个语言可以有很多不同的编译器实现,每个编译器生成的代码质量都可能不同,所以你没法说“A 语言比 B 语言快”。你只能说“A 语言的 X 编译器生成的代码,比 B 语言的 Y 编译器生成的代码高效”。这几乎等于什么也没说,因为 B 语言可能会有别的编译器,使得它生成更快的代码。

我举个例子吧。在历史上,Lisp 语言享有“龟速”的美名。有人说“Lisp 程序员知道每个东西的值,却不知道任何事情的代价”,讲的就是这个事情。但这已经是很久远的事情了,现代的 Lisp 系统能编译出非常高效的代码。比如商业的 Chez Scheme 编译器,能在5秒钟之内编译它自己,编译生成的目标代码非常高效。它可以直接把 Scheme 程序编译到多种处理器的机器指令,而不通过任何第三方软件。它内部的一些算法,其实比开源的 LLVM 之类的先进很多。

另外一些 函数式语言也能生成高效的代码,比如 OCaml。在一次程序语言暑期班上,Cornell 的 Robert Constable 教授讲了一个故事,说是他们用 OCaml 重新实现了一个系统,结果发现 OCaml 的实现比原来的 C 语言实现快了 50 倍。经过 C 语言的那个小组对算法多次的优化,OCaml 的版本还是快好几倍。这里的原因其实在于两方面。第一是因为函数式语言把程序员从底层细节中解脱出来,让他们能够迅速的实现和修改自己的想法,所以他们能 够迅速的找到更好的算法。第二是因为 OCaml 有高效的编译器实现,使得它能生成很好的代码。

从上面的例子,你也许已经可以看出,其实接近底层的语言不一定速度就快。因为编译器这种东西其实可以有很高级的“智能”,甚至可以超越任何人能做到的底层优化。但是编译器还没有发展到可以代替人来制造算法的地步。所以现在人需要做的,其实只是设计和优化自己的高层算法。

  1. 高级过程式语言

很早的时候,国内计算机系学生的第一门编程课都是 Pascal。Pascal 是很不错的语言,可是很多人当时都没有意识到。上大学的时候,我的 Pascal 老师对我们说:“我们学校的教学太落后了。别的学校都开始教 C 或者 C++ 了,我们还在教 Pascal。”现在真正理解了程序语言的设计原理以后我才真正的感觉到,原来 Pascal 是比 C 和 C++ 设计更好的语言。它不但把人从底层细节里解脱出来,没有面向对象的思维枷锁,而且有一些很好的设计,比如强类型检查,嵌套函数定义等等。可是计算机的世界 真是谬论横行,有些人批评 Pascal,把优点都说成是缺点。比如 Brain Kernighan 的这篇《Why Pascal is Not My Favorite Programming Language》,现在看来真是谬误百出。Pascal 现在已经几乎没有人用了。这并不很可惜,因为它被错怪的“缺点”其实已经被正名,并且出现在当今最流行的一些语言里:Java, Python, C#, ……

  1. 函数式语言

函数式语言相对来说是当今最好的设计,因为它们不但让人专注于算法和对问题的解决,而且没有面向对象语言那些思维的限制。但是需要注意的是并不是每个函数式语言的特性都是好东西。它们的支持者们经常把缺点也说成是优点,结果你其实还是被挂上一些不必要的枷锁。比如 OCaml 和 SML,因为它们的类型系统里面有很多不成熟的设计,导致你需要记住太多不必要的规则。

  1. 逻辑式语言

逻辑式语言(比如 Prolog)是一种超越函数式语言的新的思想,所以需要一些特殊的训练。逻辑式语言写的程序,是能“反向运行”的。普通程序语言写的程序,如果你给它一个输入,它会给你一个输出。但是逻辑式语言很特别,如果你给它一个输出,它可以反过来给你所有可能的输入。其实通过很简单的方法,可以不费力气的把程序从函数式转换成逻辑式的。但是逻辑式语言一般要在“pure”的情况下(也就是没有复杂的赋值操作)才能反向运行。所以学习逻辑式语言最好是从函数式语言开始,在理解了递归,模式匹配等基本的函数式编程技巧之后再来看 Prolog,就会发现逻辑式编程简单了很多。

从何开始

可是学习编程总要从某种语言开始。那么哪种语言呢?就我的观点,首先可以从 Scheme 入门,然后学习一些 Haskell (但不是全部),之后其它的也就触类旁通了。你并不需要学习它们的所有细枝末节,而只需要学习最精华的部分。所有剩余的细节,会在实际使用中很容易的被填补上。现在我推荐几本比较好的书。

《The Little Schemer》(TLS):我觉得 Dan Friedman 的 The Little Schemer 是目前最好,最精华的编程入门教材。这本书很薄,很精辟。它的前身叫《The Little Lisper》。很多资深的程序语言专家都是从这本书学会了 Lisp。虽然它叫“The Little Schemer”,但它并不使用 Scheme 所有的功能,而是忽略了 Scheme 的一些毛病,直接进入最关键的主题:递归和它的基本原则。

《Structure and Interpretation of Computer Programs 计算机程序的构造和解释》(SICP):The Little Schemer 其实是比较难的读物,所以我建议把它作为下一步精通的读物。SICP 比较适合作为第一本教材。但是我需要提醒的是,你最多只需要看完前三章。因为从第四章开始,作者开始实现一个 Scheme 解释器,但是作者的实现并不是最好的方式。你可以从别的地方更好的学到这些东西。不过也许你可以看完 SICP 第一章之后就可以开始看 TLS。

《A Gentle Introduction to Haskell》:对于 Haskell,我最开头看的是 A Gentle Introduction to Haskell,因为它特别短小。当时我已经会了 Scheme,所以不需要再学习基本的函数式语言的东西。我从这个文档学到的只不过是 Haskell 对于类型和模式匹配的概念。

过度到面向对象语言

那么如果从函数式语言入门,如何过渡到面向对象语言呢?毕竟大部分的公司用的是面向对象语言。如果你真的学会了函数式语言,就会发现面向对象语言已经易如反掌。函数式语言的设计比面向对象语言简单和强大很多,而且几乎所有的函数式语言教材(比如 SICP)都会教你如何实现一个面向对象系统。你会深刻的看到面向对象的本质以及它存在的问题,所以你会很容易的搞清楚怎么写面向对象的程序,并且会发现 一些窍门来避开它们的局限。你会发现,即使在实际的工作中必须使用面向对象语言,也可以避免面向对象的思维方式,因为面向对象的思想带来的大部分是混乱和冗余。

深入本质和底层

那么是不是完全不需要学习底层呢?当然不是。但是一开头就学习底层硬件,就会被纷繁复杂的硬件设计蒙蔽头脑,看不清楚本质上简单的原理。在学会高层的语言之后,可以进行“语义学”和“编译原理”的学习。

简言之,语义学(semantics) 就是研究程序的符号表示如何对机器产生“意义”,通常语义学的学习包含 lambda calculus 和各种解释器的实现。编译原理 (compilation) 就是研究如何把高级语言翻译成低级的机器指令。编译原理其实包含了计算机的组成原理,比如二进制的构造和算术,处理器的结构,内存寻址等等。但是结合了语义学和编译原理来学习这些东西,会事半功倍。因为你会直观的看到为什么现在的计算机系统会设计成这个样子:为什么处理器里面有寄存器 (register),为什么需要堆栈(stack),为什么需要堆(heap),它们的本质是什么。这些甚至是很多硬件设计者都不明白的问题,所以它们的硬件里经常含有一些没必要的东西。因为他们不理解语义,所以经常不明白他们的硬件到底需要哪些部件和指令。但是从高层语义来解释它们,就会揭示出它们的本质,从而可以让你明白如何设计出更加优雅和高效的硬件。

这就是为什么一些程序语言专家后来也开始设计硬件。比如 Haskell 的创始人之一 Lennart Augustsson 后来设计了 BlueSpec,一种高级的硬件描述语言,可以 100% 的合成 (synthesis) 为硬件电路。Scheme 也被广泛的使用在硬件设计中,比如 Motorola, Cisco 和曾经的 Transmeta,它们的芯片设计里面含有很多 Scheme 程序。

这基本上就是我对学习程序语言的初步建议。以后可能会就其中一些内容进行更加详细的阐述。

### 对的,我这里要讲的不是如何掌握一种程序语言,而是所有的…… 很多编程初学者至今还在给我写信请教,问我该学习什么程序语言,怎么学习。由于我知道如何掌握“所有”的程序语言,总是感觉这种该学“一种”什么语言的问题比较低级,所以一直没来得及回复他们 😛 可是逐渐的,我发现原来不只是小白们有这个问题,就连美国大公司的很多资深工程师,其实也没搞明白。

今天我有动力了,想来统一回答一下这个搁置已久的“初级问题”。类似的话题貌似曾经写过,然而现在我想把它重新写一遍。因为在跟很多人交流之后,我对自己头脑中的(未转化为语言的)想法,有了更精准的表达。

如果你存在以下的种种困惑,那么这篇文章也许会对你有所帮助:

你是编程初学者,不知道该选择什么程序语言来入门。 你是资深的程序员或者团队领导,对新出现的种种语言感到困惑,不知道该“投资”哪种语言。 你的团队为使用哪种程序语言争论不休,发生各种宗教斗争。 你追逐潮流采用了某种时髦的语言,结果两个月之后发现深陷泥潭,痛苦不堪…… 虽然我已经不再过问这些世事,然而无可置疑的现实是,程序语言仍然是很重要的话题,这个情况短时间内不会改变。程序员的岗位往往会要求熟悉某些语言,甚至某些奇葩的公司要求你“深入理解 OOP 或者 FP 设计模式”。对于在职的程序员,程序语言至今仍然是可以争得面红耳赤的宗教话题。它的宗教性之强,以至于我在批评和调侃某些语言(比如 Go 语言)的时候,有些人会本能地以为我是另外一种语言(比如 Java)的粉丝。

显然我不可能是任何一种语言的粉丝,我甚至不是 Yin 语言的粉丝 😉 对于任何从没见过的语言,我都是直接拿起来就用,而不需要经过学习的过程。看了这篇文章,也许你会明白我为什么可以达到这个效果。理解了这里面的东西,每个程序员都应该可以做到这一点。嗯,但愿吧。

1.重视语言特性,而不是语言 很多人在乎自己或者别人是否“会”某种语言,对“发明”了某种语言的人倍加崇拜,为各种语言的孰优孰劣争得面红耳赤。这些问题对于我来说都是不存在的。虽然我写文章批评过不少语言的缺陷,在实际工作中我却很少跟人争论这些。如果有其它人在我身边争论,我甚至会戴上耳机,都懒得听他们说什么 😉 为什么呢?我发现归根结底的原因,是因为我重视的是“语言特性”,而不是整个的“语言”。我能用任何语言写出不错的代码,就算再糟糕的语言也差不了多少。

任何一种“语言”,都是各种“语言特性”的组合。打个比方吧,一个程序语言就像一台电脑。它的牌子可能叫“联想”,或者“IBM”,或者“Dell”,或者“苹果”。那么,你可以说苹果一定比 IBM 好吗?你不能。你得看看它里面装的是什么型号的处理器,有多少个核,主频多少,有多少 L1 cache,L2 cache……,有多少内存和硬盘,显示器分辨率有多大,显卡是什么 GPU,网卡速度,等等各种“配置”。有时候你还得看各个组件之间的兼容性。

这些配置对应到程序语言里面,就是所谓“语言特性”。举一些语言特性的例子:

变量定义 算术运算 for 循环语句, while 循环语句 函数定义, 函数调用 递归 静态类型系统 类型推导 lambda 函数 面向对象 垃圾回收 指针算术 goto 语句 这些语言特性,就像你在选择一台电脑的时候,看它里面是什么配置。选电脑的时候,没有人会说 Dell 一定是最好的,他们只会说这个型号里面装的是 Intel 的 i7 处理器,这个比 i5 的好,DDR3 的内存 比 DDR2 的快这么多,SSD 比磁盘快很多,ATI 的显卡是垃圾…… 如此等等。

程序语言也是一样的道理。对于初学者来说,其实没必要纠结到底要先学哪一种语言,再学哪一种。曾经有人给我发信问这种问题,纠结了好几个星期,结果一个语言都还没开始学。有这纠结的时间,其实都可以把他纠结过的语言全部掌握了。

初学者往往不理解,每一种语言里面必然有一套“通用”的特性。 比如变量,函数,整数和浮点数运算,等等。这些是每个通用程序语言里面都必须有的,一个都不能少。你只要通过“某种语言”学会了这些特性,掌握这些特性的根本概念,就能随时把这些知识应用到任何其它语言。你为此投入的时间基本不会浪费。所以初学者纠结要“先学哪种语言”,这种时间花的很不值得,还不如随便挑一个语言,跳进去。

如果你不能用一种语言里面的基本特性写出好的代码,那你换成另外一种语言也无济于事。你会写出一样差的代码。我经常看到有些人 Java 代码写得相当乱,相当糟糕,却骂 Java 不好,雄心勃勃要换用 Go 语言。这些人没有明白,是否能写出好的代码在于人,而不在于语言。如果你的心中没有清晰简单的思维模型,你用任何语言表述出来都是一堆乱麻。 如果你 Java 代码写得很糟糕,那么你写 Go 语言代码也会一样糟糕,甚至更差。

很多初学者不了解,一个高明的程序员如果开始用一种新的程序语言,他往往不是去看这个语言的大部头手册或者书籍,而是先有一个需要解决的问题。手头有了问题,他可以用两分钟浏览一下这语言的手册,看看这语言大概长什么样。然后,他直接拿起一段例子代码来开始修改捣鼓,想法把这代码改成自己正想解决的问题。在这个简短的过程中,他很快的掌握了这个语言,并用它表达出心里的想法。

在这个过程中,随着需求的出现,他可能会问这样的问题:

这个语言的“变量定义”是什么语法,需要“声明类型”吗,还是可以用“类型推导”? 它的“类型”是什么语法?是否支持“泛型”?泛型的 “variance” 如何表达? 这个语言的“函数”是什么语法,“函数调用”是什么语法,可否使用“缺省参数”? …… 注意到了吗?上面每一个引号里面的内容,都是一种语言特性(或者叫概念)。这些概念可以存在于任何的语言里面,虽然语法可能不一样,它们的本质都是一样的。比如,有些语言的参数类型写在变量前面,有些写在后面,有些中间隔了一个冒号,有些没有。

这些实际问题都是随着写实际的代码,解决手头的问题,自然而然带出来的,而不是一开头就抱着语言手册看得仔仔细细。因为掌握了语言特性的人都知道,自己需要的特性,在任何语言里面一定有对应的表达方式。如果没有直接的方式表达,那么一定有某种“绕过方式”。如果有直接的表达方式,那么它只是语法稍微有所不同而已。所以,他是带着问题找特性,就像查字典一样,而不是被淹没于大部头的手册里面,昏昏欲睡一个月才开始写代码。

掌握了通用的语言特性,剩下的就只剩某些语言“特有”的特性了。研究语言的人都知道,要设计出新的,好的,无害的特性,是非常困难的。所以一般说来,一种好的语言,它所特有的新特性,终究不会超过一两种。如果有个语言号称自己有超过 5 种新特性,那你就得小心了,因为它们带来的和可能不是优势,而是灾难!

同样的道理,最好的语言研究者,往往不是某种语言的设计者,而是某种关键语言特性的设计者(或者支持者)。举个例子,著名的计算机科学家 Dijkstra 就是“递归”的强烈支持者。现在的语言里面都有递归,然而你可能不知道,早期的程序语言是不支持递归的。直到 Dijkstra 强烈要求 Algol 60 委员会加入对递归的支持,这个局面才改变了。Tony Hoare 也是语言特性设计者。他设计了几个重要的语言特性,却没有设计过任何语言。另外大家不要忘了,有个语言专家叫王垠,他是早期 union type 的支持者和实现者,也是 checked exception 特性的支持者,他在自己的《Kotlin 和 Checked Exception》里指出了 checked exception 和 union type 之间的关系 😛

很多人盲目的崇拜语言设计者,只要听到有人设计(或者美其民曰“发明”)了一个语言,就热血沸腾,佩服的五体投地。他们却没有理解,其实所有的程序语言,不过是像 Dell,联想一样的“组装机”。语言特性的设计者,才是像 Intel,AMD,ARM,Qualcomm 那样核心技术的创造者。

2.合理的入门语言 所以初学者要想事半功倍,就应该从一种“合理”的,没有明显严重问题的语言出发,掌握最关键的语言特性,然后由此把这些概念应用到其它语言。哪些是合理的入门语言呢?我个人觉得这些语言都可以用来入门:

Scheme C Java Python JavaScript 那么相比之下,我不推荐用哪些语言入门呢?

Shell PowerShell AWK Perl PHP Basic Go Rust 总的说来,你不应该使用所谓“脚本语言”作为入门语言,特别是那些源于早期 Unix 系统的脚本语言工具。PowerShell 虽然比 Unix 的 Shell 有所进步,然而它仍然没有摆脱脚本语言的根本问题——他们的设计者不知道他们自己在干什么 😛

采用脚本语言学编程,一个很严重的问题就是使得学习者抓不住关键。脚本语言往往把一些系统工具性质的东西(比如正则表达式,Web 概念)加入到语法里面,导致初学者为它们浪费太多时间,却没有理解编程最关键的概念:变量,函数,递归,类型……

不推荐 Go 语言的原因类似,虽然 Go 语言不算脚本语言,然而他的设计者显然不明白自己在干什么。所以使用 Go 语言来学编程,你不能专注于最关键,最好的语言特性。关于 Go 语言的各种毛病,你可以参考《对 Go 语言的综合评价》。

同样的,我不觉得 Rust 适合作为入门语言。Rust 花了太大精力来夸耀它的“新特性”,而这些新特性不但不是最关键的部分,而且很多是有问题的。初学者过早的关注这些特性,不仅学不会最关键的编程思想,而且可能误入歧途。关于 Rust 的一些问题,你可以参考《对 Rust 语言的分析》。

3.掌握关键语言特性,忽略次要特性 为了达到我之前提到的融会贯通,一通百通的效果,初学者应该专注于语言里面最关键的特性,而不是被次要的特性分心。

举个夸张点的例子。我发现很多编程培训班和野鸡大学的编程入门课,往往一来就教学生如何使用 printf 打印“Hello World!”,进而要他们记忆 printf 的各种“格式字符”的意义,要他们实现各种复杂格式的打印输出,甚至要求打印到文本文件里,然后再读出来……

可是殊不知,这种输出输入操作其实根本不算是语言的一部分,而且对于掌握编程的核心概念来说,都是次要的。有些人的 Java 课程进行了好几个星期,居然还在布置各种 printf 的作业。学生写出几百行的 printf,却不理解变量和函数是什么,甚至连算术语句和循环语句都不知道怎么用!这就是为什么很多初学者感觉编程很难,我连 %d,%f,%.2f 的含义都记不住,还怎么学编程!

然而这些野鸡大学的“教授”头衔是如此的洗脑,以至于被他们教过的学生(比如我女朋友)到我这里请教,居然骂我净教一些没用的东西,学了连 printf 的作业都没法完成 😛 你别跟我讲 for 循环,函数什么的了…… 可不可以等几个月,等我背熟了 printf 的用法再学那些啊?

所以你就发现一旦被差劲的老师教过,这个程序员基本就毁了。就算遇到好的老师,他们也很难纠正过来。

当然这是一个夸张的例子,因为 printf 根本不算是语言特性,但这个例子从同样的角度说明了次要肤浅的语言特性带来的问题。

这里举一些次要语言特性的例子:

C 语言的语句块,如果里面只有一条语句,可以不打花括号。 Go 语言的函数参数类型如果一样可以合并在一起写,比如 func foo(s string, x, y, z int, c bool) { … } Perl 把正则表达式作为语言的一种特殊语法 JavaScript 语句可以在某些时候省略句尾的分号 Haskell 和 ML 等语言的 currying 4.自己动手实现语言特性 在基本学会了各种语言特性,能用它们来写代码之后,下一步的进阶就是去实现它们。只有实现了各种语言特性,你才能完全地拥有它们,成为它们的主人。否则你就只是它们的使用者,你会被语言的设计者牵着鼻子走。

有个大师说得好,完全理解一种语言最好的方法就是自己动手实现它,也就是自己写一个解释器来实现它的语义。但我觉得这句话应该稍微修改一下:完全理解一种“语言特性”最好的方法就是自己亲自实现它。

注意我在这里把“语言”改为了“语言特性”。你并不需要实现整个语言来达到这个目的,因为我们最终使用的是语言特性。只要你自己实现了一种语言特性,你就能理解这个特性在任何语言里的实现方式和用法。

举个例子,学习 SICP 的时候,大家都会亲自用 Scheme 实现一个面向对象系统。用 Scheme 实现的面向对象系统,跟 Java,C++,Python 之类的语言语法相去甚远,然而它却能帮助你理解任何这些 OOP 语言里面的“面向对象”这一概念,它甚至能帮助你理解各种面向对象实现的差异。

这种效果是你直接学习 OOP 语言得不到的,因为在学习 Java,C++,Python 之类语言的时候,你只是一个用户,而用 Scheme 自己动手实现了 OO 系统之后,你成为了一个创造者。

类似的特性还包括类型推导,类型检查,惰性求值,如此等等。我实现过几乎所有的语言特性,所以任何语言在我的面前,都是可以被任意拆卸组装的玩具,而不再是凌驾于我之上的神圣。

5.总结 写了这么多,重要的话重复三遍:语言特性,语言特性,语言特性!不管是初学者还是资深程序员,应该专注于语言特性,而不是纠结于整个的“语言品牌”。只有这样才能达到融会贯通,拿起任何语言几乎立即就会用,并且写出高质量的代码。

Search

    微信好友

    博士的沙漏

    Table of Contents