六种编程范式:改变你对编程的思考方式

以下翻译内容均借助了GPT3.5/4,在与它的对话式学习中,我的翻译提升许多,能够达到信达雅的水平?

时不时的,我偶然找到一些编程语言,它们是如此不同,以至于改变了我对编程的思考方式。在这篇文章中,我想分享一些我最爱的发现。
这不是你爷爷奶奶那个时代的“函数式编程将改变世界”的博客文章,这个列表的内容更加玄妙,我敢打赌,绝大多数的读者没有见过下面介绍大部分的语言和范式,因此,我希望你能像我对这些新概念感到兴趣。
注:我仅有以下编程语言很少的经验,但我发现它们背后的思想很迷人,我并没有在这些领域很专精。所以请对任何错误做出指正,并且,如果你发现了任何文中没有的新的编程范式和思想,请分享给我。

默认并发|Concurrent by default

示例语言:ANI, Plaid
让我们用一个真正的思想碰撞来开始:有些编程语言天生支持并发,也就是说,每一行代码是同时执行的。
举个例子,你写了三行代码,A, B, C

A;
B;
C;

在大多数语言中,A先执行,然后是B,最后是C。然而,在ANI中,A, B, C 可以同时执行!
在ANI中代码行之间的控制流和顺序主要取决于它们之间的显式依赖之间关系。举个例子,如果B中有对A中定义变量的引用,那么A和C会同时执行,而B则在A执行完成后执行。
让我们观察ANI中的一个例子,正如教程中所描述的,ANI程序由管道和闩锁组成,它们用于操控流和数据传输。这个不寻常的句法难以理解,虽然这个语言似乎已经消亡,但是这些概念非常有趣。
这里是ANI“Hello World”的例子:

1
"Hello, World!" ->std.out

在ANI的术语中,我们发送字符串对象"Hello, World!"std.out 流,如果我们发送另外一个字符串对象到std.out 将会发生什么?

1
2
"Hello, World!" ->std.out
"Goodbye, World!" ->std.out

这两行代码并行执行,所以它们可以以任何顺序输出在控制台。现在,看看我们在一行中引入一个变量并在之后引用它会发生什么?

1
2
3
s = [string\];
"Hello, World!" ->s;
\s ->std.out;

第一行声明了一个闩锁“latch”(latch有点像变量)命名为s
其中由字符串组成;第二行将文本"Hello, World!" 发送到s;第三行解锁s并将其内容发送到std.out标准输出流。这里你可以看到ANI隐式的程序流程:由于每一行都依赖上一行,这段代码将会按照编写的顺序执行。
Plaid 语言也宣称支持默认并发,但它使用了权限模型,如这篇论文所述,设置控制流。Plaid 也探索了其他有意思的概念,比如面向类型编程(Typestate-Oriented Programming),这里状态变化成为这个语言的一等公民:定义一个对象不是作为一个类,而是作为可被编译器检查的一系列的状态和转换。正如Rich Hickey的《Are we there yet》所讨论的那样,这似乎是将时间作为一流语言结构公开的有趣做法。
多核处理器日益增长但并发编程在大多数语言中依旧是比较困难。ANI和Plaid提供了一个新颖的解决这个问题的思路并能够带来惊人的性能提升;问题则是默认并行是否使并发更难或者更容易去管理。
更新:上面的描述抓住了ANI和Plaid的基本本质,我交替使用“并行”和“并发”这两个术语,尽管它们具有不同的含义。从_Concurrency Is Not Parallelism 看更多关于这两者的讨论。
译者注:本文写于2014年,具有时效性,后发的Go和Rust在并发编程领域取得了不错的进展。

依赖类型|Dependent types

示例语言:Idris, Agda, Coq
你可能使用过像C和Java有类型系统的语言,这些编译器可以检查变量类型:整型, 列表, 或者字符串。如果,你的编译器可以检查像,一个正整数,一个长度为2的list或者一个回文字符串呢?
这个语言背后的理念是支持依赖类型:你可以指定在编译时检查变量值的类型。Scala的 shapeless library 库为Scala添加了部分实验性的支持(阅读:可能还不是准备好的黄金阶段),并提供了一些查看示例的方法。
以下是如何使用shapeless库声明一个包含1, 2, 3 三个值的向量

1
val l1 = 1 :#: 2 :#: 3 :#: VNil

这里创建了一个变量l1 ,类型签名不仅指定了是IntsVector ,还指明了Vector 的长度是3。编译器可以使用这些信息来抓捕错误。让我们来使用Vector中的vAdd 方法来对两个Vectors的元素逐一加和:

1
2
3
4
5
6
val l1 = 1 :#: 2 :#: 3 :#: VNil
val l2 = 1 :#: 2 :#: 3 :#: VNil

val l3 = l1 vAdd l2

// Result: l3 = 2 :#: 4 :#: 6 :#: VNil

以上的例子运行正常,因为类型系统知道Vectors 的长度是3,但是,如果我们尝试使用vAdd 加两个不同长度的Vectors ,我们将在编译时得到一个错误而不是在运行时。

1
2
3
4
5
6
7
val l1 = 1 :#: 2 :#: 3 :#: VNil
val l2 = 1 :#: 2 :#: VNil

val l3 = l1 vAdd l2

// Result: a *compile* error because you can't pairwise add vectors
// of different lengths!

Shapeless是一个神奇的库,但据我所知,它仍然还有一点粗糙,仅仅支持依赖类型的一些子集,并且导致相当冗长的代码和类型签名。另一方面,Idris,将依赖类型作为一等公民的语言,它的依赖类型系统似乎非常强大和简洁。作为比较的例子,可以参考谈话Scala vs Idris: Dependent Types, Now and in the Future
形式验证方法已经存在很长时间了,但通常太麻烦而无法用于通用编程。依赖类型语言像Idris,甚至未来的Scala,可能会提供轻量并且更加实用的替代方案,显著地提高类型系统捕获错误的能力。当然,由于停机问题的非内在机制,没有类型系统语言可以捕获所有的错误,但类型仍然可能成为静态类型系统的下一个飞跃。

串联式语言|Concatenative languages

示例语言:Forth, cat, joy
有没有想过没有变量和函数应用的编程会是什么样子?
没有,至少我没有,不过显然地一些人想过,并且它们想出了串联式编程。这个理念在此语言中任何函数都是向一个栈中压入数据或者弹出数据;程序几乎是通过函数组合构建的。(concatenation is composition
这听起来非常抽象,让我们来看在cat中一个简单的例子:

1
2 3 +

这里,我们压入两个数字在栈上,然后调用+ 函数,它将两个数字都弹出栈,并弹出这两个数字的加法结果。输出结果是5。这里有一个稍微有趣的例子。

1
2
3
4
5
6
7
8
9
def foo {
10 <
[ 0 ]
[ 42 ]
if
}

20
foo

让我们一行一行的过一遍:

  1. 首先,我们声明了一个函数foo。请注意,cat中的函数不能指定输入参数,所有参数都是从堆栈中隐式读取的。
  2. foo 调用< 函数,弹出栈中的第一项,将其与10相比较,然后将TrueFalse 压回栈中。
  3. 接下来,我们将数字0和42压入栈中:我们将其包裹在括号中以确保它们在未评估的情况下压入栈。这是因为它们将被分别用作调用下一行if 函数的 “then” 和 “else”的分支。
  4. if 函数从栈中弹出3个项目:布尔条件,then 分支,else 分支;依赖于布尔条件的值,它将弹出栈中“then” or “else”分支的一个结果。
  5. 最后,我们将20压入栈中并且调用foo 函数。
  6. 当所说的结束,我们得到数字42作为结果。

有关更多详细的介绍,请查看The Joy of Concatenative Languages.
这种风格的语言有一些有趣的特性:程序可以有无数种拆分和拼接的方法去创造新的程序。极其精简的语法(甚至比LISP还要精简)导致非常简洁的程序;强大的元语言支持。我觉得串联式语言是一个令人大开眼睛的思想实验,但是我不相信它的实用性。它似乎需要你记住或者想象当前栈的状态,而无法通过代码中的变量名来读取,这将使对代码的理解更加困难。

声明式编程|Declarative programming

示例语言:Prolog, SQL
声明式编程已经有很多年的历史了,但是大多数开发者依旧没有意识到它是一种概念,其要点是:在大多数主流编程语言中,你需要描述如何去解决一个特定的问题,而在声明式编程语言中,你只需描述你想要的结果,语言本身会确定如何实现它。
例如,如果你要用C从头开始编写一个排序算法,你可能会编写归并排序的步骤,如何去对半切分数据集,然后按照顺序合并它们,这里有一个例子
如果你使用像Prolog这样的声明式编程语言对数字进行排序,你会想要描述你想要的输出:我想要与这些值相同的列表,但是每一个索引为i的数应该小于或等于索引为i + 1的数,将前面的C代码与这段Prolog代码进行比较:

1
2
3
4
5
6
7
8
9
sort_list(Input, Output) :-
permutation(Input, Output),
check_order(Output).

check_order([]).
check_order([Head]).
check_order([First, Second | Tail]) :-
First =< Second,
check_order([Second | Tail]).

如果你使用过SQL,你可能已经使用了一种声明式编程方式只是可能没有意识到:当你写出select X from Y where Z 这样的查询语句时,你正在描述你想要返回的数据集;真正弄清楚如何执行这个查询的是数据库引擎。你可以在大多数数据库中使用explain语句来查看执行计划并弄清楚幕后发生的事情。
声明式语言的优点在于它们允许你在更高抽象级别上工作:你的工作仅仅是去描述你想要结果的格式。例如,一个简单的ProLog数独求解器,只需列出了解决方案中的每一行、每一列和对角线的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
sudoku(Puzzle, Solution) :-
Solution = Puzzle,

Puzzle = [S11, S12, S13, S14,
S21, S22, S23, S24,
S31, S32, S33, S34,
S41, S42, S43, S44],

fd_domain(Solution, 1, 4),

Row1 = [S11, S12, S13, S14],
Row2 = [S21, S22, S23, S24],
Row3 = [S31, S32, S33, S34],
Row4 = [S41, S42, S43, S44],

Col1 = [S11, S21, S31, S41],
Col2 = [S12, S22, S32, S42],
Col3 = [S13, S23, S33, S43],
Col4 = [S14, S24, S34, S44],

Square1 = [S11, S12, S21, S22],
Square2 = [S13, S14, S23, S24],
Square3 = [S31, S32, S41, S42],
Square4 = [S33, S34, S43, S44],

valid([Row1, Row2, Row3, Row4,
Col1, Col2, Col3, Col4,
Square1, Square2, Square3, Square4]).

valid([]).
valid([Head | Tail]) :- fd_all_different(Head), valid(Tail).

以下是将如何运行上面的数独解算器:

1
2
3
4
5
6
7
8
| ?- sudoku([_, _, 2, 3,
_, _, _, _,
_, _, _, _,
3, 4, _, _],
Solution).


S = [4,1,2,3,2,3,4,1,1,2,3,4,3,4,1,2]

不幸的是,声明式编程语言的缺陷是很容易遇到性能瓶颈,上面一个简单的排序算法的时间复杂度可能是O(n!);上面的数独求解器使用了暴力搜索;大多数开发者不得不提供数据库提示和额外的索引,以避免执行SQL查询时出现昂贵和低效计划。

符号编程|Symbolic programming

示例语言:Aurora
Aurora是符号编程的一个例子:你编写这些语言的代码不只可以包括纯文本,还有照片、数学公式、图、图表等等。这允许你操纵和描述大量不同的数据以它原生的形式,而不是都使用纯文本来描述。Aurora是完全交互式的,能够像一个功能强大的REPL一样,实时显示每一行代码的结果。

Alt text

Aurora 语言是由Chris Granger创造的,他也是 Light Table IDE的构建者,Chris 在他的博文 Toward a better programming概括了他的动机:一些使编程语言更加可观察,直接,并且减少附带的复杂度的目标。为了更多的信息,请务必查看Bret Victor令人难以置信的演讲Inventing on Principle, Media for Thinking the UnthinkableLearnable Programming.
更新:“符号编程”可能是Aurora不很准确的一个术语,从wiki Symbolic programming 看更多的信息。

基于知识的编程

示例语言:Wolfram 语言
与上文提到的Aurora语言类似,Wolfram 语言也是基于符号编程的。然而,符号层仅仅为Wolfram 语言的核心提供了一致的接口,而其核心是基于知识的编程:这个语言内置了大量的库、算法和数据。这使得从绘制Facebook 连接图、操纵图像、查询天气、处理自然语言请求,在地图上绘制方向、求解数学方程式等等各种任务变得非常容易。
Alt text

我认为Wolfram可能拥有现存编程语言中最庞大的“标准库”和数据集。我对互联网连接成为编写代码的固有部分的想法感到兴奋:这就像一个IDE,自动完成功能并进行Google搜索。观察符号编程范式是否如Wolfram 所声称的那样灵活,并且能够真正利用所有这些数据,将是一件非常有趣的事情。
更新:尽管Wolfram声称Wolfram 语言支持符号编程和知识编程,但这些术语实际上有些许差异,请参阅wiki Knowledge levelSymbolic Programming 以获取更多信息。


浩叔发起的ARTS打卡活动:每人每周写一个 ARTS:Algorithm 是一道算法题,Review 是读一篇英文文章,Technique/Tips 是分享一个小技术,Share 是分享一个观点。
此篇作为Review一篇英文文章并做了翻译,借助LLM强大的能力,我也练习了一下,将翻译水平提高些。