傻瓜式编程范式,程序员的基本功

阅读数:9019 2019 年 6 月 1 日 08:08

傻瓜式编程范式,程序员的基本功

我从数据连续性论文延伸阅读时,在 HN thread 论坛 @TuringTest 的发言引用中,意外发现了一篇文章《傻瓜式编程范式:程序员基本功》,由彼得·范·罗伊写于2009 年,描绘了设计编程语言的操作空间。如果你喜欢这篇文章,可能也会喜欢范·罗伊和哈利迪的书《 Concepts, Techniques, and Models of Computer Programming 》,因为该文的主要内容正是基于这本书。

本文介绍了所有主要编程范式、它们底层的概念以及它们的关系…我们给出了大约 30 个有用的编程范式的分类和它们的关系。

编程范式是基于数学理论或一组特定原则的方法,每一种范式支持一组概念。范·罗伊信仰多范式编程语言:解决一个编程问题,需要选择正确的概念;解决多个问题,则需要组合分属不同部分的多个概念。况且,许多程序需要解决的问题本来就不止一个。“理论上,一门语言应该以良好的方式支持多种概念,这样程序员就可以在需要时选择合适的概念,不受他人所累。”说得很直观,但我觉得也有潜在缺点:阅读这种语言的源码时,需要精通多种范式,并了解它们是怎么相互影响的。(范·罗伊可能在说“良好的方式”时考虑过如何改正这种缺点:真正的多范式语言应该避免跨范式干扰,而不仅仅是支持一大堆概念)。正如范·罗伊本人在后面讨论状态时所说的:“关键在于选择具有正确概念的范式。概念太少,则程序比较复杂;太多,则推理比较复杂。”

编程语言多如牛毛,编程范式却寥若晨星。但还是有那么一些范式的,本文将介绍 27 种有实际使用场景的范式。

下图(图 2)展示了核心理念。“值得好好学习。”每个框是一种范式;框和框之间的箭头表示从一种范式变成另一种范式时需要增加的概念。

傻瓜式编程范式,程序员的基本功

(范·罗伊原文的)图 2 主要范式图(点击查看大图

图 2 是根据创造性扩展原则(creative extension principle)组织的:

概念组合成范式并非任意为之。可以用创造性扩展原则来组织它们…在一个给定的范式中,程序可能由于技术原因变得复杂,导致与正在解决的特定问题没有直接关系。这意味着我们需要去发掘新概念了。

最常见的是,需要全局修改(而非局部修改)才能实现单个目标。(说句题外话,针对这种问题我又忍不住想给人安利 AOP(面向切面编程)了!)比如,若我们想要函数可以在任意时刻检测错误,并把控制权转给修正错误的代码,我们就需要有“异常”概念,否则将要影响所有代码。

傻瓜式编程范式,程序员的基本功

(范·罗伊原文的)图 5 异常概念是如何简化程序的

编程范式有两个关键属性:是否具有(用户)可见的不确定性,以及对状态的支持度。

…如果用户用同一套配置开始执行,结果却不同,就叫做他们见到了不确定性。这非常不可取…我们认为,只有在需要表现不确定性时,才应该让用户能够遇到不确定性。

关于状态,我们对范式如何支持及时存储一系列值感兴趣。状态可以是具名的或无名的、确定的或非确定的、串行的或并行的。并非所有组合都有用!下面的图 3 展示了一些有用的:

傻瓜式编程范式,程序员的基本功

(范·罗伊原文的)图 3 不同等级的状态支持度

主要范式图(图 2)的横轴就是按照上图中粗线组织的。

最重要的四个编程概念

最重要的四个编程概念是记录、词法范围的闭包、独立性(并发)和具名状态。

记录是若干组数据项(比如结构),可通过索引访问其中的每一项。词法范围的闭包,是把一个过程与其对外部的引用(定义闭包时引用的外部数据)结合起来。你可以创建一个‘工作包(packet of work)’在程序中传递,在之后某个时间才执行。独立性在这里指行为可以独立发生。即,它们可以并发地执行。关于并发,最受欢迎的两种范式是状态共享和消息传递。具名状态指我们可以给一个状态起名字,这是最简单的级别。但对于命名可变的状态,范·罗伊有一个更为深刻和有趣的想法:

在编程里,状态就是一个关于时间的抽象概念。函数式编程就没有时间概念…因为函数不会发生变化。现实世界则不同。在现实世界中,没什么东西像函数那样永恒不变。机体会成长和学习。机体在不同的时候受到相同的刺激,反应通常是不同的。对此,我们在程序里要如何建模?我们得创建一个具有唯一标识(它的名字)的实体模型,它的行为在程序运行过程中还会发生改变。为此,我们在程序里加入了一个抽象的时间概念。这个抽象时间概念只是一个序列,具有唯一名字的时间值的序列。

接着范·罗伊又给了一个建议,我觉得自相矛盾:“最好让具名状态永远可见:应始终提供可以从外部访问该状态的途径。”(当谈及正确性时),又说“具名状态对系统的模块化很重要”(想想《 information hiding 》)。

数据抽象

数据抽象是根据精确规则来组织数据结构用法的方法,这些精确规则保证了数据结构被正确使用。数据抽象分别有一套内部接口、外部接口和两者间的接口。

数据抽象可以按照两个主要维度进行组织:是否使用具名状态,是否将操作绑定到具有数据的单个实体中。

傻瓜式编程范式,程序员的基本功

(范·罗伊原文的)图 14 组织数据抽象的 4 种方法

范·罗伊接着讨论了多态和继承(注意,范·罗伊倾向于组合优于继承,但若你必须用继承,请确保遵循替换原则)。

并发

并发的核心问题是不确定性。

若程序的用户碰到不确定性,就会很难处理。用户可见的不确定性有时被称为竞态条件(race condition)…

若禁止不确定性,编写具有独立部分的程序将会受限。但我们能够限制不确定行为的可见程度。有两种选择:定义一种语言,所有不确定性都是不可见的;或把不确定性的可见范围限制在真正需要它的地方。

至少有四种编程模式是并发而又屏蔽了不确定性的(没有竞态条件)。表 2(下方)罗列了它们四个以及消息传递式并发。

并发范式 是否存在竞态 输入可以是不确定的 语言例子
声明式并发 Oz、Alice
约束编程 Gecode、Numerica
函数响应式编程 FrTime、Yampa
离散同步编程 Esterel、Lustre、Signal
消息传递并发 Erlang、E
表 2 四种确定性并发范式和一种非确定性的

声明式并发也称为单调数据流(monotonic dataflow)。程序接收确定性输入并用于计算确定性输出。

函数响应式编程缩写为 FRP(又名“连续同步编程(continuous synchronous programming)”)。我们在其中以函数形式编写程序,但函数参数可以更改,并且会影响输出。

离散同步编程(也称为“响应式(reactive)”),系统等待输入事件,执行内部计算,然后发布输出事件。响应式和函数响应式编程之间的主要区别是,响应式编程是离散的而非连续的。

约束

在约束编程中,我们把要解决的问题表述为约束满足问题(constraint satisfaction problem,缩写为 CSP)…约束编程是所有实际编程范式中“最声明式的”(most declarative)。

在约束编程中,不是写一组要执行的指令,而是对问题建模:将问题表述为一组变量、对变量的约束、以及实现了约束的传播器(propagators)。然后把模型传递给求解器(solver)。

语言设计指南

到这里,我们已经将一些概念和范式匆匆过了一遍,接下来我想完成范·罗伊关于设计编程语言的一些想法。有一类有趣的语言叫“双范式”语言。双范式语言通常用一种范式写小型程序,用另一种范式写大型程序。第二种范式通常用来支持抽象和模块化。比如,在面向对象语言中内置的支持约束编程的求解器。

更一般地说,范·罗伊看到了一个分层语言设计,它有四个核心层,这是一个在众多项目中都自发出现的结构:

常见语言的层次结构有四层:严格的功能核心,然后是声明性并发,然后是异步消息传递,最后是全局具名状态。这种分层结构天然地支持四种范式。

范·罗伊从他的分析中得出四个结论:

  1. 声明式编程是编程语言的核心。
  2. 在可预见的未来,声明式编程将保持其核心地位,因为分布式、安全性和容错是编程语言需要支持的基本主题。
  3. 确定性并发是一种重要的并发形式,不应忽视。这是充分利用多核处理器并行性的妙招。
  4. 用于处理一般并发的正确默认方法是消息传递,而非共享状态。

对于大型软件,范·罗伊认为我们需要采用自给自足的系统设计风格,系统可以自行配置、修复、调整等。系统将组件作为一等实体(由闭包规定),可通过高阶编程来操作。组件间通过传递消息来通信。由具名状态和事务来支持系统配置和维护。除此之外,系统本身应设计为一组联动反馈回路(interlocking feedback loops)。说到这,我想起了《系统思考和因果循环图》。

最后再说一句

每个范式都有它的“灵魂”,只有实际使用该范式才能理解。我们建议你通过在编程中实操来探索范式。

查看英文原文: https://blog.acolyer.org/2019/01/25/programming-paradigms-for-dummies-what-every-programmer-should-know/

评论

发布
用户头像
我的纯函数管道数据流, 倒是符合了傻瓜式编程范式的要求, 没在大图里找到合适的分类. https://github.com/linpengcheng/PurefunctionPipelineDataflow
2019 年 06 月 01 日 13:36
回复
没有更多了