Skip to content

本文改变自:浅谈国内高校编程语言教育 - 啥玩应啊的文章 - 知乎 https://zhuanlan.zhihu.com/p/439148421

浅谈国内高校编程语言教育

前言

众所周知,芯片是硬件的核心,同样,编程语言就是软件的核心。然而基础性学科由于其难度大、投入高、回报周期长甚至没有回报的性质,在中国一直以来不受到重视。

近年来,由于美国的科技制裁,芯片在产业界受到了重视,同时教育界也开始重视。但是,编程语言仍然处于被冷落的情况。特别地,在高校教育中,编程语言相关的课程,大多只有若干常见编程语言(如 C/C++, Java, Python, JS, SQL 等)的应用,而没有对其核心内容的讲解。

冷落的原因有以下几点:

  1. 人们的误解:“通用的编程语言全世界就那么几个,该有的都有了,我也会用,有啥可研究的?总不会让我再去设计一个新的编程语言吧?那有啥用?而且也没人用啊!”

  2. 无人为之发声:研究受到轻视导致了这门学科人才的缺失,Label A: 人才缺失意味着没有足够专业的人来为其重要性发声(编程语言学科的人才本来就不多),又因为其作为基础学科发展效果不会像应用学科“立竿见影”,国家高等教育决策层便无法主动意识到发展它的必要,因此从决策层便不会发展这一学科并重视、引进相关人才, goto Label A

  3. 教学难度大:此外,由于编程语言是一门结构性很强、在国外发展了近60年如今仍旧活跃的重要计算机学科,因此并不是只要会编程、有博士学位和相关计算机背景的人就能教好其课程的专业。如此一来,我们国家缺乏编程语言方向相关的人才,势必造成今天国内高校编程语言教育落后的局面。

  4. ……

我们需要重视编程语言吗?

“在X年内,进入世界知名大学行列;在X+Y年内,冲入世界一流大学行列”,这是近几年国内很多高校的通用口号模板。然而我们再来看看真正世界的“知名”“一流”大学(例如Top 100,包括MIT、Stanford、UC Berkeley、CMU、Cornell、UPenn、UW、Harvard、ETH、Oxford、Cambridge等太多名校),你会发现它们之中但凡有点规模的计算机院系,几乎都有编程语言(programmming languages)方向的教授或研究人员,而且往往排名越靠前的大学越重视编程语言,相关科研人员也越多,编程语言课程都是由这些专业人才讲授的。

这一对比应该很说明问题了:不是我们有问题,就是我们学习的那些世界一流大学有问题。

然而我前面也解释过了,于我而言,我国编程语言教育的落后和人才的缺失是个无从责备的问题;换言之,我相信如果咱们高校或教育决策层的领导意识到了这个问题的重要性,是有动力和能力解决它的(毕竟干实事为学生想的领导还是有的)。

提高编程语言教育质量的好处

接下来,我想从个人和社会两个层面主观地谈谈提高编程语言教育质量的好处。

个人

编程语言虽然复杂,但是其设计是遵循着一些基本规则的。

不同语言的语法语义虽然有所不同,但一些基本的核是相似的(例如基于表达式扩展的语法和对基本数据类型的抽象),编程语言可以

  1. 顺着这些核一点点展开,根据语言设计的初衷选择合适的编程范式(例如命令式还是函数式还是都要)
  2. 进而对核进行相应的调整(例如函数本身是不是个基本数据类型),一点点添加需要的规则(例如什么样的scoping,什么样的类型系统)和特性(例如是否支持多线程,支持异步)

这些规则特性与编程语言之核的组合,造就了今天编程语言的琳琅满目


然而,我们如今绝大多数国内的教育不解释这些基本原理和它们与编程语言的本质关系,学生对编程语言缺乏一种抓得住的“全局观”和举一反三的能力,看到的都是被“糖化”后的语法和复杂的语用环境,所以感到语言太复杂、枯燥。如此一来,以下情形便容易解释了。例如,

  • 你经常对编译器和解释器报出的错毫无头绪;
  • 你无法理解为什么一些framework或library的用法是这样那样的,因此你只能靠着例子和pattern来编程,对于背后被封装和使用的原理一无所知,进而导致了一旦有问题自己便推不出原因,只能找google或有求于同事;
  • 你对于大的任务该用什么样的编程语言或怎样使用其特性无法做出独立的思考,天经地义地觉得这是leader该管的事;
  • 还有太多太多心有余力不足的情况。

因此,对于个人,受到更好的编程语言教育意味着可以更好的理解从语言本身到构建出的复杂软件系统,对于问题的“势”和通过编程手段解决问题的“器”也都会有更好的把握。


编程语言的教育好比是为了构建出每个人编程知识树的主干(这种结构性很强的知识体系是很难通过经验自我摸索的),每遇到新的问题都可以用已有的编程语言知识消化吸收(或在此基础上通过查找资料更容易对其理解),进而每一步编程经历的积累都是给自己的知识树添枝增叶,日积月累,你的程序可以不知觉地写的更快,更简洁易懂,更少错,更安全,也会更容易学习理解新的编程语言和软件框架。

更重要的是其可以帮助个人对编程语言产生更浓厚的兴趣,这对于程序员健康心态的调整很重要:没有兴趣,迫于生活压力各种收罗经验编织履历向别人证明你已走多远;有了兴趣一路打怪升级顺便带带小弟时刻提醒自己还差多远但一切却又怡然自得,事业幸福感油然而生,生活幸福感便交相辉映。

社会

随着社会对于信息化的不断依赖,软件势必走向复杂化,进而对于软件可靠性、高效性、安全性等都提出了新的挑战。这意味着日后对于程序员个人编程能力和素质都会有更高的要求(我后面会在应该如何讲授编程语言课程中简单提到几点)。

因此,我国高校在培养软件人才方面应当应势所驱,重视编程语言的教育进而提高未来程序员编程素质、基本功和对待编程的兴趣,这对于整体提高处于信息化中的社会生产力是大有裨益的。随之而来的是软实力的质变,并一定会同时酝酿新技术的革新,因为基数和兴趣都得到提升,概率便不会让创造力失望。

简而言之,更好的编程语言教育可以切实地提高软件生产力,同时培育创新的沃土。例如,在大数据和智能计算的驱动下,很多行业都会利用自己领域内的数据进行一番智能变革(如医疗、城市建设、特种装备等),这给很多软件创新带来新的机遇,我们的程序员应该有能力有信心意识到,他们自己是可以创造或调制适合该行业领域应用的专属编程语言(如DSL),使得行业专家也可以很方便的使用语言编程,满足各种特定的可靠性、性能甚至知识产权等需求。这给新兴软件应用行业的创新提供了丰厚的技术资本,这些应用行业本身又会刺激市场,对计算机行业创新带来新的需求和机遇。

综上所述,重视并提高编程语言教育质量对个人和社会的发展都是大有裨益的。但是如何提高编程语言教育质量呢?最直接有效的方法就是讲授好编程语言课程。

如何教授编程语言?

1. 脉络和整体的把握

一个比较好的编程语言课程的脉络可以是从简入深地“成长”一个编程语言。这个“成长”不是基于具体语言的语法和特征的(比如Java的基本数据类型,运算符,变量声明长啥样),而是基于语言的基本概念的。换言之,上一门真正意义的编程语言课(尽管这门课指定了一门需要掌握的具体编程语言),应该主要是为了学习隐藏在这门具体语言背后的、支撑它的编程语言理论知识,而这门具体的语言(它的语法、语义、语用等)是为了理解这些编程语言知识的。

例如,编程语言可以

  1. 从数据抽象开始,通过表达式(expression)表达基本的“逻辑”和“运算”,引入条件和循环对其“控制”,引入函数对其“封装”,函数的封装需要scoping,函数的调用需要context等概念来支撑
  2. 再high-level一点,imperative语言抽象数据,用变量绑定,用状态存储;functional语言既抽象数据又抽象行为,因此可以通过表达式传递“计算”,可以无需状态存储
  3. 再high-level一点,object-oriented语言有了状态和行为的统一封装,引入了类和继承等基本概念,而后者又需要overwritten,field hidden等概念支撑等等等等。

如此一来,在一点点“成长”讲解这些编程语言知识的同时,用特定的语言的语法、语义来具体解释这些概念和知识。如此,不但能更深刻地掌握一门具体的编程语言,也能帮助了解编程语言设计的整体和核心,这是举一反三的基础,更是兴趣和自信的源泉。

当然,有的老师可能愿意把整个编程语言安排成初级和高级两个课程,在前者中讲授一门具体语言,在后者中专门讲解编程语言的理论知识,这也是一个合理的设计。但我个人更偏好一起来讲,因为理论和实践同时相互刺激能引起更强的求知欲,原因很简单,理论需要实践来解释how,实践需要理论来支撑why,一个学生如果能同时知道why和how,没有理由不愿意了解更多,虽然最后有没有兴趣是学生自己的事,但是是否引导了学生的兴趣是教育的事

2. 讲授被忽略的重要编程语言知识

  • 函数式编程

我们国内编程语言课程接触到的一般都是命令式语言(imperative)像C、Java、C++等,其中面向对象(object-oriented简称OO)的理论也有所普及。很多人听说过函数式语言(functional)但是并不了解,感觉现在找工作并没有强制要求了解函数式编程,所以觉得不学无所谓。

实际上多少了解一下函数式编程的思维更有助于理解用代码“抽象”和“计算”的概念。例如,如果普通数据value可以抽象,为什么行为(behaviour/action)不能抽象?我们可以把行为(用函数function来表达)看成像数据一样的value(first-class citizens),这样一来传入function的argument既可以是普通value也可以是行为(function)本身(进而引出higher-order function的概念),这使得function可以通过其function参数来“多态”行为,而一次计算任务就可以看成一套“行为”的组合(function的传递和调用),这样在实现一些计算任务时,代码会更简洁,逻辑会更清晰。

如今“函数式编程”被应用在越来越多的主流OO编程语言中。例如编程一霸JavaScript广泛支持了函数式编程机制;企业级代码新秀Scala一直以OO与Functional的“完美”结合标榜自己;就连OO代言人Java在Java 8后也开始通过“糖化”语法来支持函数式编程(如lambda expression)。对于一些纯函数式语言像Haskell,已经成为很多特殊行业(如金融)的宠儿。因此,教一教函数式编程(或思维)对编程语言的理解和实践都是有益的。

  • 语言的静态动态类型

有的时候编程语言可以通过“静态语言”和“动态语言”进行分类,如Scala、Haskell属于前者,JavaScript、Python属于后者。因此,有必要讲讲为什么会有静动之分,为什么不能各自取代对方,这对于理解编程语言的设计初衷和面对不同编程任务如何选择语言,都是有帮助的。

简单而言,静态语言定义变量需要声明类型,调用函数需要匹配类型等,而动态语言一般没有这种类型声明和匹配的约束。前者程序在运行前一般需要编译器静态检查这些约束,而后者程序可以直接在解释器上动态裸奔。各自好处是容易理解的,根据能量守恒原则(胡扯),静态语言需要程序员费时费力照规矩办事,面对复杂系统能大量减少运行时错误,写出的程序也容易理解和便于分析;反之,动态语言解放编程者天性想啥写啥,直接上手直接运行,轻便省力,但更容易导致运行时错误,且代码一般只有自己当天能读懂(o_O)。这也解释了为啥复杂的企业级软件青睐于静态语言,而脚本或轻量级程序一般选可以迅速上手的动态语言。

理解了这些,也就更容易理解为啥有时候编程语言会“动中向静”(JavaScript和TypeScript,动态语言静态化),有时候会“静中有动”(Java和Reflection,静态语言通过metaobject开了个动态的口子),有时候会“静动兼修”(Gradual typing)。

3. 拓展一些常见的编程语言知识

有的时候拓展讲解一点常见的编程语言知识会更有趣且有启发性。我举两个简单的例子:继承和异常处理(两个都拿常见的Java语言为例吧)。

  • 继承

相信面向对象课程都有针对继承的讲解(毕竟OO三大件是封装、继承、多态),比如Java中的类和父类的继承关系以及继承带来的好处。但是往往我们有一种倾向,就是觉得存在即合理,因此觉得继承一定就是好的,进而通常并不讲解Java中这种基于类的继承的弊端。

实际上这种Java基于类的继承方式被很多PL的人吐槽过(当然只吐槽它不好的地方),因为它在很多情况下会造成不必要的冗余和开销(简单说就是你可以和你爹很多基因一样,但是不代表你俩穿的衣服都是一样的)。

实际上很多继承关系是可以被更轻量级的composition比如Mixin技术代替的(虽然不像Scala、Ruby等语言在语言层面原生态支持Mixin,Java在Java 8后通过interface default methods也可以间接实现Mixin)。有了这些了解,也可以更容易理解其它继承设计的初衷和意义了,比如JavaScript这种基于prototype的更轻便的继承方式。

  • 异常处理

说到异常处理最直观的感受就是一种编程语言提供的处理错误的机制。很多程序任务都不可避免地要处理各种可预判的错误,例如访问的文件不存在,取值的对象为空等等,在异常处理机制下,错误可以标准化也可以定制化,不同的错误可以根据其类型通过try/catch捕捉处理。实际上,在此基础上如果再拓展讲一点,学生也许能站在更高(更抽象)的层面看到编程语言背后的设计哲学。这一点就是“异常处理”背后体现的“逻辑分流”设计。

程序本身有“主体逻辑”(以期待的输入进行任务计算完成输出),然而这一过程不可避免地要处理“错误逻辑”。如何让程序以及编程者的主体逻辑思维不受错误逻辑干扰(代码也会随之简洁易懂)是异常处理机制设计的一个重要初衷。有了上面“逻辑分流”的理解,学生以后看到例如JavaScript的Promise这样的特性(resolve和reject分流),就会更容易理解该特性设计的初衷并愿意在合适的编程场合使用该特性。

以上,我觉得对一些基础知识(如上面讲的“继承”“异常处理”等)拓展地讲解,学生更容易跳出某个语言本身,更容易站在编程语言设计层面上去思考(进而将知识融汇贯通形成自己的知识树),甚至有可能进一步思考各种奇形怪状语言的雏形,这种兴趣的代入和思考的启发应该是教育的重心。我也相信这种“由内而外”的“漫天想象”会带来日后很多有生命的创新。

4. 对于语义的重视

编程语言的语义非常重要,学习好语义可以从本质上理解编程语言,因为它在教你运行(该语言写的)程序的解释器是如何思考工作的。大家知道很多通用编程语言有很长的规范来具体地描述语言的语法、语义和特性以及它们应该怎么被使用,很多市面上的编程语言书籍(例如XX程序设计语言,XX handbook,XX in action太多了)也是参考这些规范文档来撰写的。然而无论是官方的规范手册还是书籍都是用自然语言描写的,这就会产生很多歧义和覆盖不到一些特殊情况。

这也意味着看书学习一个一个语法特性编程尝试后,也很难对语言有一个整体的了解,对于语言局部的细节也很难有准确的把握。

在这种背景下,编程语言的形式化语义显得尤其重要,它通过一套基于数学的形式化的描述,通过语言的语法定义其相应的语义(注,一般只定义语言的核心部分,因为实际的通用编程语言很复杂,很难形式化准确定义描述所有其特性语义,如此一来整个语言和其使用环境并不能够得到严谨准确地描述和证明,因此可以说如今的编程语言包括编译器和运行环境实际上都会有bug,只是很多都还没有暴露而已)。编程语言领域语义的研究有着较长的历史,很多国外顶级大学的编程语言课程都会有形式化语义的专门讲解,描述语义的方法也有很多(如denotational semantics,operational semantics等)。

然而,尽管编程语言语义很重要,我们国内很多高校的编程语言教育忽略了这一块,据我所知,只有少数高校编程语言课上会单独讲授包括语义在内的语言理论知识,例如北京大学的王捍贫、熊英飞等老师、南京大学的梁红瑾、冯新宇老师等。

实际上,如果不是单独的程序语言理论课程也不用全面细致地讲解它们(这样可能会有些枯燥,毕竟我们不是法国人( ̄▽ ̄"))。例如,如果编程课程选择学习的语言是Java,可以考虑把整个Java的核心部分的语法和语义拿出来,挑选一种语义方法比如operational semantics里的small-step state machine方法来描述并一一讲解语言的每个核心语法和其对应的语义,这样学生在掌握了描述语法、语义方法的同时,也在整体和核心细节上理解了Java,这非常有益于日后更好地理解编程语言并编写程序。

当然,我前面说了这么多的内容,很多时候不可能在一个单独的课程中都覆盖到,不同高校可以结合自身的实际情况,可选择性地为学生提供不同的PL课程。

总结

这篇文章从“写作动机”开始,先后针对“为什么我国不重视编程语言教育”,“好的编程语言教育会对个人和社会带来什么好处”,“应当如何讲授编程语言课程”几个问题逐一阐述了我的个人观点。从来没有花过这么多时间和心思写这种观点性的文章,但是写完又有一种内心的满足,因为如同在文中“写作动机”所述,“希望让更少的学生被惋惜,让更多的学生受鼓舞”,尽管只是编程语言这一小领域,我仍庆幸自己迈出的这一小步。

知乎是一个挺好的地方,虽然如今感受到了更多的浮躁,但仍然能看到人们对于知识的渴望和对于自己进步的渴求,尤其是那些在更新速度极快的计算机领域中的学生和程序员们。鉴于自己在国内受过的编程语言教育和在知乎上看到的形形色色的编程语言问题,我切实地感受到了我国编程语言教育的相对落后,如文中所述,这对信息时代下的个人和社会的发展都会带来不可估量的损失。

我虽资质平平,但仍想借此文发声:我国高校主管计算机和软件教育的领导们,各计算机学院软件学院的院长们,人工智能、物联网、大数据等计算机应用固然重要,但请不要忽视计算机基础、软件核心“编程语言”的教育和研究,毕竟所有应用软件都是编程语言写的,毕竟如今的计算机行业对程序员的“质”的要求越来越高,毕竟我们有着这么多渴望编程知识和兴趣的准程序员们。期待我国编程语言教育和科研的进步。

此致。