欢迎光临散文网 会员登陆 & 注册

计算机语言学习导论

2023-04-25 04:33 作者:幻の上帝  | 我要投稿

计算机语言学习导论

  因为 B 站限制,外部链接等格式无法显示。略去部分历史内容。嵌套列表限制 2 层。

原文档见以下地址:

https://github.com/FrankHB/pl-docs/blob/master/zh-CN/introduction-to-learning-computer-languages.md

引言

  因为近几个月收到了较多计算机科学基础尤其是编程语言入门指导的问题,之前也说过好几回会汇总一起解答,是时候填坑了。

  但说实话这个坑挺大的,无法一次性填完,在这里会不定期更新。

  因为是填坑,所以请读者当读到 TODO 注释时不要感到奇怪。大部分这类注释表示文档中相对较低优先级的部分,缺失可能会影响完成度,但不应影响主旨的理解。

概述

  计算机语言是在计算机上为处理信息等目的而设计和实现的人工语言(artifical language) ,包括通用目的语言(general-purpose language) 和领域特定语言(DSL, domain-specific language) 两类。

  人工语言和自然语言(natural language) 相对,最显著的区别是人工被人为地设计(design) 而非演化(evolute) 。自然语言的演化过程中不排除正字法(orthography) 等的人为干预,但主要的现象仍然来自演化的结果。而人工语言不需要遵从受到不可控环境演化规律的影响,原则上完全使用人为指定的规则(rule) 。

  一般地,GPL 具备可编程性(programming) ,能表达程序代码。这类语言称为编程语言(PL, programming language) 。DSL 可能是 PL 。其它的领域特定语言可能仅为了描述数据格式而不要求使其代码被作为程序,因此不视为 PL 。

  广义的语言学(linguistics) 同时以自然语言和人工语言两者为研究对象,但初始的对象仅是自然语言。有的人工语言(如世界语)是人为创造的自然语言替代品;与此不同,计算机语言首要便于计算机处理,而非以自然语言的方式使用,它一定是书面语。语言学中,处理自然语言的一些分支,如语音学(phonetics) ,在计算机语言中不再适用。因此,可以认为计算机语言是具有更大限制性的语言。

  但因为侧重的目的过于不同,不仅研究对象的普遍规律不同,传统的语言学的方法论也经常不在计算机语言上生效;并且,完善的人为设计能排除重新发现和应用外部规律的需要。因此,指导计算机的不是语言学,而是编程语言理论(PLT, programming language theory) ,这可被认为是一种数学分支。其它的不具有足够可编程性的计算机语言,仍然能被 PLT 解释。

入门策略

  要入门,首先需要看到门在哪里。

场景

  对初学者而言,一般首先通过某一门具体 PL 来接触使用语言编写程序这样的主要场景。但是,尽管大多数学习者首要关心这个场景,学习语言和学习编写程序实际上是两回事。学习语言的知识至少还是改进语言设计这样的任务场景的基本要求。因此,“门”的指涉也相应地不同:

  • 如果只是想要编写程序,入门可以是了解环境的搭建并实际写出可以运行的程序。

  • 如果是学习语言,入门是掌握如何参照基本规则适应不同的使用场景的一般方法。

  编写程序是一个主要场景,但不是全部。作为更一般地情形,后者的要求显然高于前者。对后者,“门”具体是指权威的语言规则

规范

  作为人工语言,这些规则应当是明确的。一般计算机语言的规则通过语言规范(specification) 或称为规格说明的文档描述。由于计算机语言的实现(implementation) 通常是软件,这同时也是语言实现的软件规格说明书的公共组成部分——具体实现可能有更具体的扩展。例如:

  • Java 语言的语言规范是 JLS(Java language specification) ,通过 JVMS(Java virtual machine specification) 提供一些实现的更具体的规范。

  • C 语言的语言规范是国际标准 ISO/IEC 9899 ,具体实现的规格说明由提供实现的各厂商自行维护。

  语言规范应当确保提供足够完整的语言规则,这些规则定义了符合性(conformance) ,即判定一个实现是否构成语言规范中的规则定义(define) 的语言。符合性规则包含了对实现的要求(requirement) 。因为实现不需要被无条件要求能接受任意输入,语言规范通常也规定对实现可接受程序的要求。因此,语言规范能够定义清楚什么样的代码才是符合要求的程序,而间接明确了如何写出正确的程序的最低要求

  因为语言规范中语言规则不依赖具体实现细节,掌握语言规范和其它一些 PLT 和实现的知识,用户应当就能自行实现语言。满足符合性的语言实现之间允许存在实现环境要求和支持特性集等差异,而形成被实现的语言的不同的方言(dialect) 。语言规范中的符合性规则同时也是检验不同方言之间的兼容性的准则。

  理解和掌握如何去寻找这些明确的语言规则,可以认为是入门了。就那么简单。

现实应对

  然而实际情形更加复杂一些。有时语言只有一种实现,可能不提供语言规范,而直接发布实现(如解释器和编译器),同时给出指导如何使用实现的参考手册(reference manual) 。参考手册是提供给用户的具体实现的使用说明书,通常是规格说明的简化版。此时权威来源只此一家,由于人工语言的特殊性,用户没法擅自添加规则(除非同时维护自己的实现),所以参考手册会被勉为其难地被作为语言规范来使用。这样的语言如 Python 和 Rust 。

  作为对比,参考手册只需要描述具体实现的行为和使用方式,不一定提供完整的语言规则,因此通过参考手册开发方言虽然不是不可能(如 Python 在 CPython 之外还有 PyPy 等),但因为规则不够清晰,难度可能更大,出现更多的兼容性问题。

  严格来说,没有语言规范的人工语言设计是残缺的。不过,对实用计算机语言这样复杂的情形,语言规范的起草要求较高,乃至工业语言的设计者自身都不一定有能力独自完成(而需要专家委员会或志愿者协助),使用参考手册凑数作为妥协也并不少见。

  其它的文献,例如关于 PL 的教科书,一般都是二次开发作品,在描述什么是语言上权威性更弱。有少数的例外:语言的原始设计者在起草语言规范前提供的替代文献。例如,C++ 标准的第一版(即 ISO/IEC 14882:1998 )之前,Bjarne Stroustrup 的 The Annotated C++ Reference Manual 虽然体例和名义上是参考手册,实际起到了语言规范的作用(当时已有多个语言实现),因为没有更权威的完整语言规则的来源。

  遗憾的是,大多数新手只接触到如何入门编写程序的文献,甚至这些文献在编写程序上的入门都很不完善,却没有意识到基础的不完整。结果,先接触这些文献的用户,可能了解在特定情形下如何写出可运行的程序,但实际上没有能力在最基本的情形保证总是按照预期运行。

  例如,首先试图用编译器生成的汇编代码来解释 C 程序如何运行的用户,通常不会理解 C 的未定义行为(undefined behavior) 的具体外延和排除方法。一旦更换编译器版本、操作系统、具体程序输入甚至什么都不变,程序就可能罢工,但这种差异的来源并非是生成的代码,而可能是因为按照语言规范(这里是 ISO C )源程序自身存在缺陷,连维持正确的最低要求都没达到。

  一旦习惯这种思路,要进一步确保写的程序能符合最低正确性要求的、可被人理解,往往比纯新手入门还困难,因为首先要摒除错误姿势的形成的思维定势。

  因此,作为初学者,应当注意,即便仅是为了学习编写程序而不是全面掌握语言的实用,这样地一知半解入错门还不如不入门

局限性

  应当了解,语言规范规定关于语言的规则并不一定在语言外适用。特别地,它可能不适用于概括不同语言的不同规则,乃至术语都不通用。对要求多语言(ployglot) 理解的一般情形,通常应了解 PLT 的惯例。

  但是,一般初学者不足以有条件分辨到底哪些语言中的定义符合一般惯例。这种情形下,须对具体语言提供的定义保持足够的怀疑,即默认不要把未经确认的术语和规则套用到其它语言中,以免形成错误理解和以讹传讹。

  一些体现现有具体语言的复杂和混乱情形的实例:

  • 一些重要基本概念,如变量(variable) 和对象(object) ,定义在各个语言中存在出入。

    • JLS 规定变量(variable) 是带类型的存储,Go 语言规范也类似,而这实际相当于 ISO C 和 ISO C++ 中对象(object) 的概念。

    • ISO C 没有变量的正式定义。

    • ISO C++ 的变量总是具名的。这符合数学传统,同时和 ISO/IEC 2382 的定义兼容。

    • Java 和 Go 语言的定义和数学传统以及遵循这个传统(“变量的值不可变”)的纯函数式语言(如 Haskell )中的定义(特别地,类型变量(type variable) )是冲突的。

    • 事实上,JLS 中直接定义了类型变量,是符合数学传统的;但类型变量不是变量,直观上存在矛盾。

  • ISO C++ 规定多态类(polymorphic class) 是具有虚成员函数的类,但实际上这里的多态性(polymorphism) 在一般意义上在一般意义上的特殊情形,即[包含多态(inclusion polyrphism) 或称子类型多态(subtype polymorphism)] 。

    • 尽管没在语言规范中明确,C 和 C++ 的强制(coercion) 是一种特设多态(ad-hoc polymorphism) 。

    • C++ 的重载(overloading) 是另一种特设多态。

    • C++ 的类模板是参数多态(parametric polymorphism) 。

    • 上述其它形式的多态在历史上早于包含多态的出现。

  此外,由于大多数语言的设计者未必具有足够的 PLT 背景,他们的作品不一定能够正确反映一般语言的知识。语言的实现者也未必足够清楚关于设计上的知识。例如:

  • Python 的设计者承认不清楚尾调用和循环的区别。

  • Go 语言的设计者看来不清楚名义类型(nominal typing) 。

  • Lisp 方言的一些编译器作者使用尾调用优化(TCO, tail call optimization) 但并不清楚真尾递归(proper tail recursion) 的一般性质,造成一些混乱。  

  看起来可能是专家的语言设计和实现者尚且如此,没有足够参与语言维护工作的一般用户的观点同样(如果不是更)可能是相当地不可靠的,这包括大多数的高校教师。因为大学的具体 PL 课通常是基础课,可以理解通常难以配备足够的专家作为师资。

  而可能出人意料的是,相当一部分的教科书作者也不可靠。因为编写教科书的要求和语言规范不同,为了容易理解,可能更难以保证正确性

  更困难的一个问题是,几乎任何一个实用计算机语言其实都已经复杂到不适合没有完全没有理论计算机背景知识的学习者。而很多基础课(如关于体系结构或者算法的)却又经常要求有学生有 PL 的基础,否则难以理解其中“为何如此设计”的原理——因为它们要么是适应 PL 的局限性要么是会使用 PL 的方法描述内容,根本无法彻底抽象掉。

  所以真正的零基础入门,无论只是为了学习编写程序,还是更一般地掌握 PL ,唯一比较可行的就是选择比较简单的实用通用目的 PL ,然后了解如何使用 PL 的主要方式,再在必要时根据领域选择不同的具体 PL 进一步学习。

文献选择

  如果有条件,例如已经有其它类似的 PL 学习基础,且语言规范文档容易获得,那么应当直接使用语言规范。

  若研究需要,则优先使用补充的第一手来源(论文、报告和专利等)。仅当同行评审或教学需要时,才有必要参考二手资料。

  如果要仅针对特定的语言选读非权威文献,则一般建议选择语言设计者的著作,这至少能接近原始的设计。

  而 PLT 则视为单独的领域。但是,PLT 有非常多细分领域,即便是知名的著作也未必在每个方向上都保证可靠。如果需要了解,请务必做好考察原始文献(主要是论文)的准备

  对零基础入门,实现上节的策略建议选择 SICP 2nd edition ,这是 MIT 课程编号 6.001 的同名经典课程的经典教材(没有之一)。理由是:


  • 此书使用 Scheme 作为教学 PL 。

    • Scheme 的核心语言特性相对简单但很灵活,程序代码也较紧凑,不用花费多少时间学习即可以求解描述较复杂的问题,零基础适应编程方法这个目的性价比较高。这在书中有直接体现。

    • Scheme 语言规范 RnRS 以简洁而紧凑著称,篇幅较短信息集中,体例较为严谨,质量相对较高,适合初学者熟悉语言规范的写作风格以及练习从语言规范文档检索信息。

    • Scheme 要求支持真尾递归,确保尾递归和循环在计算复杂度上等价,避免一开始需要记忆过多的控制结构语法,有助于养成对过程式范型(procedural paradigm) 的正确认识。

    • 作为通用 PL ,Scheme 在函数式编程(functional programming) 、DSL 和动态语言等多个方面都可以作为典型的实例直接使用。

    • 作为早期 LISP 的演进,也和其它 Lisp 方言在很多特性上通用;同时还直接影响了一些流行语言如 JavaScript 。了解 Scheme 对学习这些语言有帮助。

    • Scheme 具有天然的 PLT 背景,是该方向的学习者入门的起点之一(尽管 SICP 并没有详细展开)。

    • 正确地展示了如何合理使用不同形式的递归,而非流于表面和纠结具体实现细节。

    • 涉及过程式、函数式和逻辑编程(logical programming) 等范型。

    • 明确了续延(continuation) 和求值器(evaluator) 等基础概念。

    • 有的实用编程技巧(如 streams )经常被误认为进阶内容,在此接触有助于避免日后一些不必要的神秘感。

  • 作为入门教材,基本没有局部错误和偏离主题的现象,不容易对其它领域或其它 PL 的进一步学习造成先入为主的负面影响。

    • 也许除了可能嫌弃其它语言不够灵活……

    • 作者之一是 Scheme 的主要设计者,PL 上基本不用担心专业性问题。

  • 作为经典,久经考验。

  • 可以找到原作者教学的视频资源。虽然其实看书就够了。

  但同时要注意,除非直接在 MIT 上课,最好不要使用 Python 作为教学 PL 的新版本:

  • 尽管也能完成基本的课程目标,Python 版本基本上缺乏所有以上 Scheme 具有的优点。

    • Python 在零基础的意义上没有 Scheme 简单,例如需要纠结语句和表达式。

    • Python 没有正式语言规范只有语言参考文档,体例质量和通读难度上都不如 RnRS 。

    • 在初期接触 Python 的一些限制性设计(如 lambda 表达式只有一行,没有真尾递归)不利于养成良好的编程习惯。

    • 学了 Python 再学别的语言可能不那么有参照性。

  • 新版质量相对较差,例如关于语句和表达式直接并列的讲法不严谨,容易造成错误理解。

  • 新版替换掉了一些经典内容,虽然更接近当前实用场景,但也意味着这部分内容更容易被其它“现代”的参考文献替换。

  • 使用这个版本的一个理由是和数据科学课程衔接,因为该领域流行使用 Python ,能减少学习工作量。

    • 不过这方面只会 Python 实际可能不大够用(比如经常还得会 C++ ),所以这个入门姿势收益可能不一定明显,而影响以后不太有机会纠正的基础理解明显是亏的。

  其它要点:

  • 如果日程紧凑,需要安排其它课程,可以只看前 3 章。

    • 因为这里的依赖不是线性的,前面的章节和入门这个目的相关性较高,后期的内容可以作为进阶内容学习。这时可以针对性参考其它更详细的文献而非入门读物。

  • 如果日程仍然紧凑,可以跳过习题。

    • 但需要自行另外验证学习效果。

做题

  一般顺序是靠谱的教材的题目做一部分或者全部。应试随意。

  有些题可以看看但不去解。在不解题的情况下理解出题的思路不一定比解题容易,可能比解题还重要。不过这要求正确判断问题在涉及问题领域中的重要程度,对初学者较难。

  除非对竞赛感兴趣,刻意刷所谓的“算法题”对一般从业者没多大必要。因为实际干活的场景比单纯实现算法经常麻烦得多,搞不好配个环境都有技术门槛,这种不容易训练的技能更该趁早对付。刷题除了有时间限制,某种意义上只是简化了规则的舒适区。

  笔者的经验比较极端:因为入门就是从语言手册和规范文档开始,之后才参考普通的著作(包括 SICP ),原则上不是为了挑作者的错就全部跳过题目,而直接用解决项目中的实际问题代替;偶尔兴起直接出题再比较出题思路。这么做:

  • 首要好处是节约学习时间。特别地,减少训练正确高效地提出和总结问题这样的(相对如何实现还可能更重要的)研究和工程技能所花费的时间。

  • 稳定工作流:

    • 为什么重复造轮子,现成的不香?

    • 非要自己实现……如何实现?肌肉记忆不够?

    • 剩下的……世界那么大,旮旯那么多,临时找文献都比回忆怎么做题快。

    • 找不到,没人会,不得不自己整了,横竖都别指望快哪去。(做题也不太容易练这个。)

  • 从提问入手,更容易发现现有的缺陷。

    • 有的需求就是假需求,有的问题压根就不该存在。

    • 极大地提升了看穿过度设计的能力。

  • 主要坏处是对基础的全面要求比较高。

    • 比较吃天赋,特别吃想象力:需要能预判没经验的具体场景并承担误判风险,免得浪费时间掌握个空气还不如老实做题。

    • 入门时没轻没重,问题都怎么没见识过所以不太会正确评价具体问题的重要程度,实操很容易在分配资源上跑偏。

  • 题外话:

    • 不是复杂或偏门到一定程度,如何写出能跑的代码其实是相对容易替代的没那么智能的工作。(看看 AI 。)

    • 看代码(发现问题或为代码质量背书)比写代码可能更有技术含量。

    • 在能确保可行的前提下,通常正确理解需求和提出设计比具体实现更关键

    • 如果花了过多的时间在纯粹的实现而非设计和验证设计的过程上,可能就该怀疑干的是不是太偏体力活了。

    • 只接到体力活没办法?摸鱼去学多点有技术含量的啊……其实调 bug 可能倒不那么体力活。

  总之:

  1. 量力而为。

  2. 只做题死路一条。

  3. 有自信可以完全不做题。

语言律师

  有的人会把理解语言规则的人称作语言律师(language lawyer) ,这并不怎么确切。律师通常需要执业资格,而所谓语言律师并不是。并且,除了律师和法官等专业人士外,普通人也有必要了解一些法律常识;即便不是要维护语言的演化而只是利用语言来写代码的语言的普通用户,掌握语言规则的最基本知识,也同样有必要。

  另一方面,从法的位阶来说,大部分语言规范并没有经过广泛的公开程序评审而更像“地方自治法规”,公开的技术标准才更像是“法律”。

  这实际上确实在文本上有所体现,例如,ISO/IEC directives, part 2 对情态动词使用 shall 而非 must 表示强制要求(mandatory requirement) 以及 should 表示建议的规定,和实际的英语国家中的传统法律的遣词习惯一致。当然,这种规范性主要强调形式统一,而具体表现,也有与之相同和不同的做法:

  • RFC 2119 规定关键词 MUST、REQUIRED 或 SHALL 都能表示上述 shall 的含义。

  • RFC 8174 更新 RFC 2119 ,进一步规定只有大写形式才具有特殊含义。

  • 关于法律中的 shall 和 must ,存在一些长期误用的歧义,因此学界存在争议。

  • 英国法院裁定英国竞争和市场管理局终止对苹果的反垄断调查,因为苹果上诉主张 shall 的含义是强制要求被法院支持。

  实际情况:入门阶段不太容易遇到需要律师解决的问题。

  不要为逃避本该掌握的常识问题找借口。

TODO

TODO 补充其它进阶话题。

TODO 补充和具体语言相关的问题。


计算机语言学习导论的评论 (共 条)

分享到微博请遵守国家法律