编程范式浅析

1 理解编程范式

编程范式(Programming paradigm),也称编程范型、程序设计法

范式是一种思考或处理问题的方式,也决定了编程语言的风格和能力

编程语言与编程范式之间是具体与抽象的关系,就像文学作品与文学流派一样,根据某些相似的文学作品,可以从中提炼出思想与主旨构建与支持文学流派,而文学流派也可以指导并引领出更优质的文学作品

不存在一种占据绝对优势的编程范式,也永远不会有一种编程语言一直独领风骚,取长补短,融汇百家之长是计算机与编程语言得以生机盎然的主要原因

如今流行的大部分编程语言都是多范式的,比如使用C++编写程序时,可以是面向过程编程,也可以是面向对象编程,也可以混着用

2 编程范式简史

2.1 荒芜中野蛮生长

机器语言是第一代编程语言,由0和1组成各种命令,以纸带或板卡为载体,效率极高但不具备可读性,难以学习和修改,且可移植性差

汇编语言是第二代编程语言,汇编语言实现了初步的可读性,程序运行时会通过编译器将汇编语言转化为机器指令,从此机器开始逐步向程序员妥协

之后的第三代与第四代编程语言存在很多功能的重叠,二者的分界也不再清晰,在本文中统一称呼为”高级语言”,高级语言追求的是可读性、跨平台的通用性、面向开发者的友好性

在高级语言发展的初期阶段,编程语言还没有严格的规范要求,编程风格比较随意,因为程序中的流程可以随意跳转(goto机制),所以不同人实现同一个复杂功能的代码,称得上是千人千”面”,百花缭乱,十面埋伏,一气呵成

任何事物都有对立的两面,很多人也非常推崇这份乱象背后所展现出的充分自由,任何天马行空的构想都能通过编程实现,虽然最终的代码有的如小孩涂鸦般滑稽稚嫩,却也有很多惊艳卓绝之辈留下了如莫扎特谱曲般优雅的作品

舌尖上的编程反面模式(编程范式反面教材)

  • 面条式代码(Spaghetti code):控制流如一盘面般盘根错节,常见于设计不合理、跳转混乱或频繁修改导致的复杂非结构化程序
  • 馄饨式代码(Ravioli code):因为过度的分离和封装,程序结构由很多如馄饨般松散连接的小部分构成,虽然耦合性低,但是代码堆栈臃肿,可读性差
  • 千层面代码(Lasagna code):不同层次间的代码如千层面般互相纠缠不清,程序结构缺少分离和封装,所以代码耦合性强,修改困难,牵一发而动全身

2.2 从混沌走向秩序

1966年,科拉多·伯姆(Corrado Böhm)及朱塞佩·贾可皮尼(Giuseppe Jacopini)在著名的ACM期刊发表论文,提出了结构化程序理论(也被称为伯姆-贾可皮尼理论),并认为所有可计算函数都可以通过顺序、选择及重复这三种程序的组合实现。

1968年,艾兹赫尔·戴克斯特拉(Edsger Wybe Dijkstra)发表了著名的文章《GOTO语句有害论(Go To Statement Considered Harmful)》,文中首次提出了"结构化编程(structured programming)"的概念,并认为GOTO语句增加了程序的分析和验证难度。

1970年,IBM的研究员哈伦·米尔斯(Harlan Mills)提出了更具通用性的“结构化编程”,并应用于纽约时报存档系统的开发,并取得成功,引得其他公司纷纷效仿。

结构化编程酝酿于1950年代后期,并在20世纪60年代至70年代期间逐步兴起,直至20世纪末,才实现广泛的认可与推行,此前诸如FORTRAN、COBOL、BASIC等缺乏编程结构的高级编程语言,也开始逐步支持结构化编程。

结构化编程使得代码的通用性得到释放,在开源思想的引导下,每个开发者都能贡献出自己星星之火,最终汇聚形成燎原之势,彻底点燃了属于互联网的信息新时代

以是否满足结构化作为分界点对编程范式进行区分,将早期不符合结构化但能实现具备图灵完备性算法的程序设计模式称之为非结构化编程,而符合结构化的则称之为结构化编程。如今所能接触的所有的高级编程语言基本都属于结构化编程。

图灵完备性(Turing Completeness):

  • 这种性质是针对一套数据操作规则而言的概念
  • 图灵完备性指能够实现图灵机模型里的全部功能
  • 图灵机是一种理论数学模型,用于解决任何可计算的问题
  • 通俗解释,图灵完备性就是能够解决所有可被人类计算的问题

值得一提的是,虽然非结构化编程已经逐步退出程序设计的舞台,但是其中蕴含的一部分编程思想却也一直被传承着。比如饱受诟病的GOTO语法,这套机制本身并没有太多问题,只是过于充分的灵活性拉低了代码的理论质量下限。而一些GOTO语法的变种至今仍在发挥着重要作用,比如循环语句中的breakcontinue,这些都属于受限制的GOTO语句。

2.3 盛世中百花齐绽

伴随着信息时代的浪潮,各种编程范式的流派层出不穷,百家争鸣。其中的主流编程范式发展方向可以大致分为以下三种角度思考:封装抽象、限制规范与专业化。

  • 模块化抽象

代码的结构规范是编程风格从混沌走向秩序的重要分水岭,而不同的代码块之间其实还可能存在依赖性或者局部相似性,而为了更好地把握这部分特性以提高代码的复用性和可拓展性。对于代码块进行更深入的模块抽象便成了自然而然的选择,面向对象编程便是在这一思想下诞生的集大成者,其中基于类的封装、继承和多态等机制,已经称为这一时代大部分编程语言的必备特性。

更有趣的是,伴随着第五代编程语言的概念兴起,代码不再追求过程描述和细节实现,而是追求以目标为导向,并对实现过程进行了更深入的封装。比如深度学习领域的Prompt范式,能够通过任务描述和提示实现特定任务的自主建模和优化,虽然其中依然存在很多不足之处,但其展现出的巨大潜质已足以令人惊叹。

  • 限制规范

事物总是存在两面性,抽象模块化显著提高了编程效率,却也带来一些限制与约束。以命令式编程为例,指令是命令式编程的核心概念,其作用本质上是对变量的状态修改,而修改的依据则是数据与算法,命令式编程的程序结果也就是变量值。因此简单概括来说:命令式编程=纯数据+算法+状态维护

这种对于状态的维护看似是合理的,实则在多线程环境下面临挑战,当多个线程对变量状态同时进行维护时,变量的结果便不再是可确定的,这时就需要通过“加锁”的操作,保障同一时刻只有一个线程能修改变量的状态,但是这在高并发场景下又会极大的拉低程序的运行效果(Python便因为一个全局解释器锁GIL的设计而导致效率低下,引人诟病)

为了解决这一问题,必须改变命令式编程“用算法改变数据”的核心思想,比如通过封闭函数和变量的上下文环境,使得函数内变量的状态仅与函数(运算逻辑)相关,而函数本身具备特定的生命周期和专属内存块,函数在输入参数时激活,在输出值后释放。而这就是声明式编程中的函数式编程的核心概念-闭包(closure)。

在实际运转中,函数式编程还需要引入yield等概念确保生命周期是可测的、可维护的, 但不可否认,通过限制对变量状态的直接维护,函数式编程将算法和变量实现了合理的分隔与约束,进而收获了更高效、简洁的自动内存管理机制,也实现了在高并发场景下的天然优势。

《架构整洁之道》精彩片段

  • 结构化编程是对程序控制权的直接转移进行限制与规范(限制指令方向)
  • 面向对象编程是对程序控制权的间接转移进行限制与规范(限制数据作用域)
  • 函数式编程是对程序的赋值操作进行限制与规范(限制数据的可变性)
  • 没有一个范式是在增加编程的能力,而是在约束编程的方式
  • 编程范式在过去几十年的发展中,学到的最重要的是——什么不应该做

编程语言的限制规范引起了很多人的不满,因为人们不能再对代码实现完全的掌控,在实现某些天马行空的想象也面临着诸多不便。但是这种限制与规范大大降低了编程语言传播和复用的门槛,使得很多知识实现了某种意义上的实质沉淀,进而使得新的思路与构想可以快速实现,并再次沉积下来,为下一位继往开来者提供准备。这种周而复始的沉积与提升,才是信息时代得以蓬勃发展的根本原由。

  • 专业化

一个概念的发展就如同植物幼苗的生长,伴随着与概念相关的知识丰富与结构完善,幼苗也在慢慢地开枝散叶,从而形成一个科目的雏形。由于成长环境的差异,幼苗的后代也演变出了各种特性,以适应复杂多变的生态环境,由此引发的多样性促进了一类学科的诞生。

编程范式亦可如此理解,代码的严苛标准使得人们可以实现知识的共享与传播,这塑造了一个学科的广度。而为了应对复杂多变的生产场景,编程范式也在不断演化,以更好地适应不同垂直领域的特异性需求,这构建了一个学科的深度。

具体来说,面对数据存储业务,类SQL编程语言用于处理结构化数据流;面对数值模拟计算业务,MATLAB语言的实现更贴近数学的计算与思考方式;面对网页设计业务,HTML能快速构建页面的结构与主要内容,CSS则页面赋予了样式与变化,JS则在网页动态效果方面具备天然的优势性。除了以上提及的,这类垂直领域还有很多,比如统计分析与报告(SAS)、最优化(Lingo)、神经网络设计与建模(Torch)等等。

而更多的语言,具备一定的通用性功能的基础上,也会在某些领域具备更多的优势,比如R语言在数理统计方面颇具特色,而Python在数据分析和建模方面也具备最强的适用性。总的来说,编程语言的发展在垂直领域具备着“用进废退”的原则,编程语言在某个领域具备优势,该领域的人才也会更多地使用并改进编程语言,使得此编程语言在该领域具备更强的优势性,这也许就是开源与社区文化下滋养的编程语言的天命与轮回吧~

3 编程范式分类

不同的编程范式之间只是看待问题的角度不同,既可能存在交集,也可能互相独立。因此编程范式的类型很难进行单纯的归类,本文仅从发展演变、思考差异等角度出发列举一些常见的几种编程范式。

3.1 非结构化编程VS结构化编程

非结构化编程是历史上最早的能够创造图灵完备算法的程序设计模式,实现简单功能时快捷高效,但支持数据类型少,可读性差,修改困难,不适用于较为复杂的项目。

非结构化编程典例:汇编语言、早期版本的BASIC

相比于非结构化编程,结构化编程引入了代码的模块化设计,并通过“自顶向下,逐步求精”的程序设计方法和限制goto语言,使得程序结构清晰易读,避免了代码重复,方便测试与调试,能够应对复杂的项目需求,是现代编程语言的基础范式。

结构化编程典例:C语言、Java

3.2 命令式编程VS声明式编程

命令式(Imperative)编程,又称指令式编程,最古老的一种编程范式,因为最初的计算机硬件都是通过接收一系列的指令来完成任务的。因此命令式编程侧重于描述程序的逐步运行,命令式编程一般流程清晰,逻辑严谨,运算效率较高。

命令式编程还可以细分出很多常见的编程范式,比如过程式编程、面向对象编程等。目前市面上的大部分编程语言都是命令式编程,比如C、Java、C++等。

理解与补充:结构化编程与命令式编程来自看待事物的两种视角,二者之间不存在包含或被包含的关系,非命令式编程可以包含模块化设计,而命令式编程也同样可以包含GOTO机制

不同于命令式编程,声明式(Declarative)编程侧重于对程序预期结果的高级描述,即不再指定程序实现所需结果的所有细节,所以声明式编程代码简洁、实现高效,但可能需要付出额外的计算成本,影响程序性能。

声明式编程也可以细分出很多优秀的编程范式,比如函数式编程、逻辑式编程等。声明式编程典例:类SQL查询语言、正则表达式等

4 常见命令式编程范式

4.1 过程式编程

过程式编程(Procedural programming),即面向过程(procedure-oriented)编码,其核心概念在于过程调用(procedure call),其中的过程也可以称为程序、子例程(subroutine)或函数

过程的本质是一种代码的模块化,所以过程式编程是结构化编程的一种实现形式。而子例程(代码块组成的可调用单元,可以先简单理解为函数)中封装着针对特定任务的程序指令序列,所以过程式编码也是命令式编码的子集

过程式编码采用自上而下的方法,程序跟踪调试方便,代码可重复使用,可移植性强,但需要对函数进行额外的管理,并且调用子例程可能产生额外的计算开销。大多数早期的编程语言都是过程式的,比如:C语言、Pascal、BASIC

理解与补充:命令式语言并不一定为过程式编码,比如纯汇编语言是命令性语言,但不是结构化编程,也不属于过程式编程

4.2 面向对象编程

面向对象编程(,简称OOP)的核心原则依然是“封装”,但是这种封装的抽象层次比过程式编码更高。基于类/对象的封装是OOP的一种典型形式,类通过封装、继承、多态等特性,使得代码块具备更高的复用性和可拓展性。

一个常见的错误观点是认为过程式编程(面向过程编程)和面向对象编程是对立的,但其实面向对象编程依然包含着过程的思想,类中方法或函数都可以是过程式的。

面向对象编程编程典例:C++、Python、Java

5 常见声明式编程范式

5.1 函数式编程

函数式(functional)编程,又称泛函编程,其核心思想主要来自数学概念中的“函数”,函数是一种对于自变量的映射,一个函数的结果仅取决于输入参数的值。依靠这种思想,函数式编程减少了很多变量的状态声明与维护。

函数式编程有很多特点,比如用函数递归替代循环用于流程控制,可以实现惰性求值(求值过程在真正需要用到值的时候才触发),还引入了高阶函数(函数可以作为其他函数的输入或输出)和闭包(函数+引用环境)等概念。

函数式编程代码简洁,可读性强,伴随着业务场景的复杂化,这种简明快捷的编程范式变得逐渐流行,诸如C++、Java等老牌编程语言都在后续的升级中引入了Lambda 表达式用于支持函数式编程。函数式编程典例:JavaScript、Haskell、Scala

5.2 逻辑式编程

逻辑式(logic)编程将程序编写为一系列遵循逻辑结构的事实与规则(比如:当X为真且Y为真时,Z为假),计算机依托于这些逻辑构建出的知识框架进行推理计算。

逻辑式编程语法灵活简单,但受到逻辑规则的全面性与合理性的限制,是一个富有潜力的发展方向。逻辑式编程典例:Prolog、Datalog、Mercury

6 其他常见编程范式

6.1 数组式编程

数组式(Array)编程是对于标量操作的泛化,允许将计算或操作直接应用到整个数据集(向量、矩阵、高维数组),从而规避单个标量的显示循环(一般也可以具备循环的能力),也非常适合进行隐式的并行化。

数据式编程实现了数据泛化操作的简洁表达,并且符合数学的计算模式,但是为了实现高度的抽象与封装,在部分操作计算上的效率可能有所损耗。数组式编程典例:MATLAB、Octava、Julia、R语言

6.2 符号式编程

符号式(Symbolic)编程将计算过程抽象为计算图,并对其中输入、计算和输出都进行了符号化处理,类似于数学中从数值计算到符号计算的演变。符号式编程具备一定的自我优化和学习能力,因此常用于人工智能、专家系统、计算机游戏等领域。深度学习框架(Torch、Tensorflow)常通过构建计算图的方式实现神经网络的设计与计算推理

符号式编程借助计算图的优化和传递闭包的特性,内存占用少、计算速度快,但是对程序调试不友好,通常不作为单独的编程语言出现。符号式编程典例:LISP、Prolog

7 编程范式简单展望

每种编程范式都有其独特与亮眼之处,而实际中的绝大部分编程语言都会汲取多种编程范式的精髓,围绕“多范式”构建优雅而实用的语言特性。或许编程语言会随着时代而更迭演变,但这些编程范式将会沉淀下来,成为滋养下一个优秀编程语言的沃土。

8 编程范式补充资料

编程语言及其对应的多范式汇总表 主要编程范式的演变流程图 常见垂直领域及其编程语言 常见编程语言及其分类 编程语言的历史

9 参考文献

CTMWiki wiki-编程范式 wiki-编程范式对比 wiki-结构化程序理论 知乎-编程范式杂谈 简书-Tensorflow与符号编程 你应该知道的 5 个编程范式 “主要的编程范型”及其语言特性关系 Programming Paradigms: A must know for all Programmers 命令式编程、过程式编程和结构化编程有什么区别?

往年同期文章