译者注:本文介绍了六种编程范式,提到了不少小众语言,作者希望借此让大家更多的了解一些非主流的编程范式,进而改变对编程的看法。以下为译文:
时不时地,我会发现一些编程语言所做的一些与众不同的事情,也因此改变了我对编码的看法。在本文,我将把这些发现分享给大家。
这不是“函数式编程将改变世界”的那种陈词滥调的博客文章,这篇文章列举的内容更加深奥。我敢打赌大部分读者都没有听说过下面这些语言和范式,所以我希望大家能像我当初一样,带着兴趣去学习这些新概念,并从中找到乐趣。
注:对于下面讲到的大多数语言,我拥有的经验其实很少:我只是觉得他们背后的思想很赞,但对于它们没有任何相关的专业知识,所以有任何更正和错误请指出。此外,如果你发现任何没有包括在这里的新的范式和想法,请分享它们!
更新:这篇文章上了r/programming和HN首页,感谢反馈,我将会进行更正。
默认支持并发(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这门语言似乎已经死了,但概念还是相当有趣的。
下面是ANI中的“Hello World”示例:
"Hello, World!" ->std.out
在ANI语法中,我们将“Hello World!”对象(一个字符串)发送到std.out流。如果我们发送另外一个字符串到std.out会怎样?
"Hello, World!" ->std.out
"Goodbye, World!" ->std.out
这两行代码并发执行,所以它们可能以任意顺序在控制台输出。现在,看看当我们在一行中引入一个变量并在之后引用会发生什么:
s = [string\];
"Hello, World!" ->s;
\s ->std.out;
第一行声明一个叫做s的“锁存器”(锁存器有点像变量),其中包含一个字符串;第二行发送文本“Hello World!”发送到s;第三行“解锁”s并将内容发送到std.out。因此,你可以看到ANI的隐式程序排序:因为每一行运行都依赖于上一行,因此,这段代码将按照它编写的顺序执行。
Plaid语言也声称默认情况下支持并发,但使用的是本篇论文中所描述的一种权限模型来构建控制流。 Plaid还探讨了其它有趣的概念,如面向类型状态的编程,在那里状态转换成为了语言中的重要因素:你定义的对象不再是类,而是一系列可以由编译器检查的状态和转换。看起来十分有趣,正如Rich Hickey在演讲“Are we there yet”中所讨论的,将时间作为语言结构的首要因素。
Multicore正处在上升期,并发性仍然比大多数语言更难。ANI 和 Plaid 对于这个可能产生惊人的性能提升的问题提供了一个新的思路;不过问题是“默认支持并行”是否让并发更容易或难以管理。
更新:上面的描述讲解了ANI和Plaid的基本本质,但我可互换地使用术语“并发”和“并行”,即使它们有不同的含义。如果想了解更多信息请阅读“并发不是并行”这篇文章。
依赖类型 (Dependent types)
示例语言:Idris, Agda, Coq
你可能习惯于像C和JAVA等语言的类型系统,编译器可以检查一个变量是整数,列表,或者字符串。但是如果你的编译器可以检查一个变量是“正整数”,“长度为2的列表”,还是“一个回文字符串”会怎样呢?
这就是支持依赖类型语言背后的思想:你可以在编译时指定检查变量值得类型。Scala的Shapeless库添加了对Scala依赖类型的部分实验性质支持,并提供了观察一些例子的简单方法。
下面是如何声明一个Vector的代,其中使用了shapeless库,包含值1、2、3:
val l1 = 1 :#: 2 :#: 3 :#: VNil
这里创建了一个变量l1,它的类型签名不仅指定它是一个包含Ints的Vector,还指定了它的长度是3。编译器可以使用这个信息来捕获错误。让我们使用Vector中的vAdd方法来执行两个Vector间的成对相加(pairwise addition):
val l1 = 1 :#: 2 :#: 3 :#: VNil
val l2 = 1 :#: 2 :#: 3 :#: VNil
val l3 = l1 vAdd l2
// Result: l3 = 2 :#: 4 :#: 6 :#: VNil
上面的例子运行正常,因为类型系统知道两个Vector的长度都是3.。然而,如果我们尝试两个长度不同的Vectors,我们会在编译时得到一个错误。
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是一个了不起的库,但在我看来,它仍然有点粗糙,只支持依赖类型的一个子集,并导致生成相当详细的代码和类型签名。另一方面,Irdris,使类型成为编程语言的首要成员,所以,依赖类型系统似乎更强大和更干净。为了比较,可以看看“Scala VS Idris:从属依赖类型的,在现在和未来”演讲。
形式化验证方法已经存在很长一段时间了,但往往过于繁琐,不适用于通用编程。依赖类型的语言,如Idris,甚至在未来的Scala中,可能会提供更轻量级和更实用的替代方案,这仍然可以显著的提高类型系统捕捉错误的能力。当然,由于终止问题的固有限制,没有哪个依赖类型系统可以捕捉到全部错误,但如果做得好,依赖类型可能是静态类型系统下一个大的飞跃。
拼接语言(Concatenative languages)
示例语言:Forth, cat ,joy
想象过,在没有变量和函数应用的情况下,编写程序是什么样子的吗?没有?我也没试过。但显然有人做了,他们提出了拼接编程。这个概念背后的思想是语言中的都是把数据压入堆栈或者弹出堆栈的函数;程序几乎完全通过功能组合来构建(基于堆栈的编程语言)。
这听起来相当抽象,所以让我们来看cat语言中一个简单的例子:
2 3 +
在这里,我们将两个数字推倒堆栈上,然后调用+函数,它将两个数字从堆栈中弹出,并将它们相加的结果添加到堆栈:代码的输出是5。下面是一个更有趣一点的例子:
def foo {
10 <
[ 0 ]
[ 42 ]
if
}
20
foo
让我们逐行解读上面的代码:
更详细的介绍,请参看“ The Joy of Concatenative Languages ” :http://www.codecommit.com/blog/cat/the-joy-of-concatenative-languages-part-1
这种编程风格有一些有趣的属性:
我发现拼接编程是一个令人大开眼界的思想实验,但我还未实践过。似乎你必须记住或想象堆栈的当前状态,而不能够从代码中的变量名读取它,这会使代码很难理解。
声明式编程(Declarative programming)
示例语言:Prolog, SQL
声明式编程已经存在了许多年,但大多数程序员仍然不知道它是怎样的概念。简单来说:在大多数主流语言中,开发者是在描述如何解决一个特定的问题;在声明式语言中,你只需要描述你想要的结果,而语言本身确定如何到达那里。
例如,如果你使用C语言从头开始写一个排序算法,你可能会为合并排序写一个说明,一步一步的描述如何递归地将数据集分割成两部分并将其合并到一起:这里是一个例子。如果使用声明式语言如Prolog来进行数字排序,可直接描述你想要的输出:“我想要相同的值列表,但每个索引i中的每个项目都应小于或等于索引为i+ 1的项”。将前面的C语言解决方案和下面的Prolog代码进行对比:
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语言中一个简单的数独求解器的代码只需要列出每行,每列,和一个解决的数独难题的对角线应该看起来的样子:
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).
下面是如何运行上面的数独求解器:
| ?- 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是完全交互式的,它会立即显示每行代码的结果,像steroids中的REPL。
Aurora语言是由Chris Granger创造的,他还创建了Light Table IDE。克里斯在他的文章《为了更好地编程》中描述了创建Aurora的动机,目标是使编程更直观,直接,减少偶然的复杂性。欲了解更多信息,请参见Bret Victor的演讲:Inventing on Principle, Media for Thinking the Unthinkable, and Learnable Programming。
更新:“符号编程”可能不是适用于Aurora。更多信息参见维基百科上的“符号编程”的维基主页。
基于知识的编程(Knowledge-based programming)
示例语言:Wolfram语言
与上面提到的Aurora语言很像,Wolfram语言也是基于符号编程的。然而,符号层仅仅是提供一种与Wolfram语言核心一致的接口,Wolfram语言是基于知识的编程语言:内置了大量的库、算法和数据。这使得可以轻松地从图形化的Facebook连接,到处理图像,查找天气,处理自然语言查询,绘制地图上的方向,解决数学方程等等。
我猜想Wolfram语言拥有现存语言中最大的“标准库”和任何现有语言的数据集。我被“互联网连接(Internet connectivity)是代码编写的固有部分”这个想法所打动:它就像一个通过谷歌搜索来实现自动完成功能的集成开发IDE。这将是非常有趣的,看看符号编程模型是否像Wolfram声称的那样灵活,可以真正利用所有这些数据。
更新:虽然Wolfram声称Wolfram语言支持“符号式编程”和“知识编程”,但这些术语的定义是有所不同的。更多信息请参阅 Knowledge level 和 Symbolic Programming 的维基主页。