编程语言介绍/编程语言范式
编程语言可以粗略地分为两类:命令式和声明式。然而,这种分类并不严格。它仅仅意味着一些编程语言更自然地促进了开发程序的特定方式。命令式编程侧重于如何做某事,而声明式编程则表达了给定问题的解决方案是什么。声明式语言可以进一步细分为函数式和逻辑语言。函数式语言将计算视为数学函数的求值,而逻辑语言将计算视为公理和推导规则。
命令式语言遵循在图灵机中描述的计算模型;因此,它们保留了状态的基本概念。在该形式化中,程序的状态由存储器磁带的配置以及规则表中的指针给出。在现代计算机中,状态由存储在内存和寄存器中的值给出。特别是,一个特殊的寄存器,即程序计数器,定义了要执行的下一条指令。指令是命令式编程中的一个非常重要的概念。命令式程序向机器发出定义要执行的下一个操作的命令。这些操作会改变机器的状态。换句话说,命令式程序类似于食谱,其中定义并排序了执行某事的必要步骤。指令组合在一起构成命令。命令主要有三种类型:赋值、分支和序列。
- 一个赋值通过使用新值更新其内存来改变机器的状态。通常,赋值由一个表示内存位置的左值和一个表示将存储在该位置的值的右值表示。
- 一个分支通过更新程序计数器来改变程序的状态。分支的示例包括诸如 if、while、for、switch 等命令。
- 序列用于将命令串联在一起,从而构建更具表现力的程序。某些语言需要一个特殊符号来指示命令的结束。例如,在C中,我们必须用分号来结束它们。其他语言,如Pascal,需要在构成序列的命令之间使用一个特殊符号。在 Pascal 中,这个特殊符号也是分号。
下面是用 C 编写的程序说明了这些概念。此函数描述了如何获得一个整数的阶乘。要实现这一目标,我们首先要做的就是将数字 1 赋值到名为 f 的内存单元中。接下来,当名为 n 的内存单元中存储的值大于 1 时,我们必须使用该单元的值乘以 n 的当前值来更新 f,并且我们必须将存储在 n 中的值减少 1。在这些迭代结束时,我们得到了输入 n 的阶乘,存储在 f 中。
int fact(int n) {
int f = 1;
while (n > 1) {
f = f * n;
n = n - 1;
}
return f;
}
命令式语言是工业界占主导地位的编程范式。有很多假设可以解释这种主导地位,对于良好的讨论,我们可以推荐 Philip Wadler 的优秀论文。命令式语言的例子包括C、Pascal、Basic、汇编器。
还有一些其他多范式语言也部分甚至完全支持命令式范式,例如 C++、JavaScript,但作为多范式语言,它们不是很好的例子,因为这些语言的实际使用并不符合描述。
声明式语言中的程序声明一个真理。换句话说,这样的程序描述了某个真理成立的证明。这些程序更关心问题的解决方案“是什么”,而不是“如何”获得该解决方案。这些程序由表达式构成,而不是命令。表达式是编程语言中任何返回值的有效语句。这些语言具有一个非常重要的特性:引用透明性。此属性意味着任何表达式都可以用其值替换。例如,计算 5 的阶乘的函数调用可以被其结果 120 替换。声明式语言进一步细分为两个非常重要的类别:函数式语言和逻辑语言。
函数式编程基于称为lambda 演算的形式化。与图灵机一样,lambda 演算也被用来定义哪些问题可以通过计算机解决。函数式范式中的程序类似于我们在数学中使用的符号。函数没有状态,所有数据都是不可变的。程序是许多函数的组合。这些语言已经普及了一些技术,例如高阶函数和参数多态性。下面是用Standard ML编写的程序,是阶乘函数。请注意,这个版本的阶乘与我们之前用 C 编写的程序有很大不同。这一次,我们没有解释如何获得阶乘。我们只是说明给定数字 n 的阶乘是什么。在声明式世界中,通常使用事物来解释自身。数字n的阶乘是乘法链n * (n-1) * (n-2) * ... * 3 * 2 * 1。我们事先不知道n有多大。因此,为了提供n的阶乘的一般描述,我们说这个量是n乘以n-1的阶乘。归纳地,我们知道如何计算最后一个阶乘。鉴于我们也知道如何将两个整数相乘,我们得到了最终的量。
fun fact n = if n < 1 then 1 else n * fact(n-1)
如今,有许多函数式语言在使用。除了Standard ML之外,值得注意的例子还包括Ocaml、Lisp、Haskell和F Sharp等语言。函数式语言在软件行业中没有被广泛使用。然而,已经有一些用这些语言编写的非常成熟的项目。例如,Facebook 社交网络的聊天服务是用函数式语言Erlang编写的。
逻辑编程是声明式编程范式中的第二个子类别。逻辑程序将问题描述为公理和推导规则。它们依赖于称为统一的强大算法来证明关于公理的属性,给定推理规则。逻辑语言建立在称为霍恩子句的形式化之上。逻辑编程语言家族中只有少数成员。其中最著名的是Prolog。函数式编程中发现的引用透明性的关键属性也存在于逻辑编程中。这个范式中的程序并没有描述如何找到问题的解决方案。相反,它们解释了这个解决方案是什么。下面是用 Prolog 编写的程序,计算阶乘函数。就像 SML 程序一样,这个版本的阶乘函数也描述了数字的阶乘是什么,而不是必须执行哪些计算才能获得它。
fact(N, 1) :- N < 2.
fact(N, F) :- N >= 2, NX is N - 1, fact(NX, FX), F is N * FX.
Prolog 和相关语言在工业界中的使用甚至比函数式语言更少。然而,逻辑编程在学术界仍然非常重要。例如,Prolog 和 Lisp 是人工智能爱好者首选的语言。
在某些语言中,开发命令式程序更加自然。同样地,在某些编程语言中,开发声明式程序(无论是函数式还是逻辑式)更加自然。
范式选择可能取决于多种因素。从解决问题的复杂性(面向对象编程部分起源于简化和建模复杂问题的需要)到程序员和代码可互换性、一致性和模式化等问题,都是为了持续努力将编程变成一种工程化和工业化的活动。
然而,在决策的最后阶段,可以说编程哲学更多的是程序员的决策,而不是编程语言本身的强加。例如,以下用 C 编写的函数以非常声明式的方式找到了阶乘
int fact(int n) { return n > 1 ? n * fact(n - 1) : 1; }
另一方面,也可以在声明式语言中创建阶乘函数的命令式实现。例如,下面我们有一个用 SML 编写的命令式实现。在这个例子中,结构 ref 表示对内存位置的引用。感叹号 ! 读取存储在该位置的值,而命令 while 与命令式语言中的语义相同。
fun fact n =
let
val fact = ref 1
val counter = ref n
in
while (!counter > 1) do
(fact := !fact * !counter;
counter := !counter - 1);
!fact
end;
顺便说一句,我们观察到函数式语言的许多特性最终在命令式语言的设计中找到了作用。例如,Java 和 C++ 今天依赖于参数多态性,以泛型或模板的形式,为开发人员提供构建更可重用软件的方法。参数多态性是静态类型函数式语言中常见的特性。这种迁移的另一个例子是类型推断,今天在 C# 和Scala 等语言中不同程度地存在。 类型推断 是静态类型函数式语言中常见的另一个特性。这种最后一种编程语言 Scala 是不同编程范式在现代编程语言设计中如何相遇的一个很好的例子。以下用 Scala 编写的函数,取自该语言的教程,是著名的快速排序算法的命令式实现
def sort(a: Array[Int]) {
def swap(i: Int, j: Int) { val t = a(i); a(i) = a(j); a(j) = t }
def sort1(l: Int, r: Int) {
val pivot = a((l + r) / 2)
var i = l
var j = r
while (i <= j) {
while (a(i) < pivot) i += 1
while (a(j) > pivot) j -= 1
if (i <= j) {
swap(i, j)
i += 1
j -= 1
}
}
if (l < j) sort1(l, j)
if (j < r) sort1(i, r)
}
if (a.length > 0)
sort1(0, a.length - 1)
}
下面我们用相同的语言以更声明式的方式实现了这个算法。很容易看出声明式方法更加简洁。一旦我们习惯了这种范式,可以说声明式版本也更加清晰。然而,命令式版本效率更高,因为它在原地进行排序;也就是说,它没有分配额外的空间来执行排序。
def sort(a: List[Int]): List[Int] = {
if (a.length < 2)
a
else {
val pivot = a(a.length / 2)
sort(a.filter(_ < pivot)) ::: a.filter(_ == pivot) ::: sort(a.filter(_ > pivot))
}
}