2.1 重温编程范式
一般来说,解决问题的思路是先找出问题的来源,然后将问题分解成若干个小问题逐一解决。同时,还需要考虑所有情况,以保证该方法能完整地解决问题。而编程范式就是一种将编程活动分解的思想,虽然编程范式有很多种,但一种范式可能更适合解决某一类问题。
在这里只讨论命令式、逻辑式、函数式和面向对象的编程范式。
建议大家理解编程范式,因为一种编程语言可能适合使用一种特定的范式。例如:C语言适合使用命令式编程范式、Haskell适合使用函数式编程范式、Smalltalk适合使用面向对象的编程范式。下面具体介绍这些不同的编程范式。
2.1.1 命令式编程范式
命令式编程范式是一种程序化的编程方式,它主要关注的是变量和可能会改变这些变量值的语句(代码段)的顺序执行过程。这种范式基于冯·诺依曼计算机,其具有可重复使用的内存,并且允许改变这块内存的状态。
命令式编程范式假设计算机能够维持在计算过程中生成的变量的不同状态。对于命令式编程范式而言,程序执行的顺序非常重要,如果更改语句顺序,则可能会产生不同的状态和结果。
在程序运行过程中,计算变量的状态是程序中定义的变量的当前值、将要执行的下一个任务或语句以及任何活动子程序所期望调用的数据。以上这些是可以改变程序状态的因素,因此命令式编程范式可以被理解为一种连续执行语句的范式。
使用命令式编程范式具有如下优势:
• 能有效地利用系统资源。
• 基于计算机的运行方式,因此与机器语言相近。
• 很多流行的语言都使用这种编程范式。
使用命令式编程范式具有如下劣势:
• 很多问题都无法按照顺序执行的方法来解决。
• 缺乏引用透明性,这意味着变量的状态可以改变,使得程序难以理解。
• 调试不简单。
• 只能实现有限的抽象类型。
2.1.2 逻辑式编程范式
逻辑式编程范式也称为基于规则的编程范式。它基于谓词逻辑,是解决问题的一种声明性方法,其侧重于关系。比如Prolog就是一种逻辑式编程语言。
这样的程序可以分为三个部分:
• 定义和声明、定义问题的域。
• 在相关域中给定问题的事实。
• 一些表达式得出的结果。
逻辑式编程范式没有控制语句,只有关系描述。用户不需要了解程序的运行过程。因此,Y = f(X)等价于表达式r(X,Y),其中r代表一种关系,它定义了X和Y的关系。在基于规则的编程中,我们只需要提供事实(规则和公理),然后通过变量赋值来推测一些语句的证明。
另外,程序也可以从任意方向进行计算。例如:
• 当X已知时,可以计算Y。
• 当Y已知时,可以计算X。
这里给出一个定义关系的例子:兄弟—父亲—母亲—男性。
【范例2-1】关系
下面的代码展示了一些关系。
01 male(X) // X是一个男性 02 father(F,X) // F是X的父亲 03 father(F,Y) // F是Y的父亲 04 mother(M,X) // M是X的母亲 05 mother(M,Y) // M是Y的母亲
这里定义了5种关系,它们都是r(参数)的形式。通过以上关系可以推测出brother定义了兄弟的关系。
brother(X,Y) // X是Y的兄弟
当然,逻辑式编程也有一些缺点:
• 可以以任意方式计算的函数其执行的效果不好。
• 基于规则的编程仅限于使用关系表达的域。
2.1.3 函数式编程范式
函数式编程范式源于纯粹的数学意识形态:功能理论。它将所有的子程序都视为函数。函数(在数学意义上)接收参数并在计算后返回结果。结果取决于该函数的计算,而计算取决于我们为函数提供的输入参数。
连续状态在函数式编程范式中无效。函数的结果将会是另一个表达式的输入,不会被保存为变量。函数式编程范式比其他范式更简洁、更简单,因为它遵循数学函数理论。函数是函数式编程范式中的第一类对象。函数可以被视为一种数据,假设函数将返回一个值,这允许我们将函数作为参数传递给另一个函数,或者从其他函数返回一个函数。
我们试着理解函数式编程范式,并将其与命令式编程范式进行比较。比如创建一个函数,将输入数据映射到在命令式编程范式中执行n条语句时可能获得的结果。
Stat指一个声明,Stat_0, Stat_1, Stat_2, …,Stat_n是n+1个声明。
现在,根据函数式编程范式,有:
F(Stat_0) = Stat_n
此函数将初始状态映射到最终状态。
接下来,我们将其分解为单个表达式,这将表示每条语句的结果。
F(Stat_0)= Stat_n(Stat_n-1(...(Stat_1(Stat_0))))= Stat_n
也可以写成:
F = Stat_n 0 Stat_n-1 0 ... Stat_1
因此,可以通过为每条语句构造一个函数并以相反的顺序执行它们,将程序从命令式编程范式转换为函数式编程范式。虽然这不适用于所有情况或问题,但基本思想是相同的。
函数式编程范式的特点和优势如下:
• 函数提供了高级抽象,从而降低了错误的可能性。
• 程序独立于赋值操作,可以编写高阶函数,这使得函数式编程范式适用于并行计算。
• 与命令式编程范式不同,函数式编程范式保持引用透明性,这使它更适合于数学表达式。
• 函数式编程范式中的值是不可变的。
函数式编程范式也有如下一些缺点:
• 在某些情况下,函数式编程范式变得很复杂。比如通常在需要处理大量顺序活动时,通过命令式或面向对象的编程范式可以更好地处理这些活动。
• 某些程序可能不如使用其他范式编写的程序有效。
2.1.4 面向对象的编程范式
在面向对象(OOP)的编程范式中,对象是抽象出来的真实世界的实体,对象具有行为或方法,通过行为或方法可以对对象的状态进行修改。面向对象的编程范式将重点放在对象上,这些对象属于特定类,类具有对象可以使用的特定方法。由于对象是真实世界的实体,因此它们是封装的,包含可以更改数据状态的数据和方法。
面向对象的编程范式基于4个主要原则。
(1)封装:限制从外部访问内部的方法。有权访问对象的方法只能操纵它们的状态,可以防止外部方法更改对象的状态以执行无效的操作。
(2)抽象:这是一种使用类在接口和功能方面定义概念边界的方法,可以保护对象的内部属性。
(3)继承:允许类从现有类继承属性和行为,从而无须重写它们,这也有助于保持一致性。因为如果有变化,我们只需要在一个地方进行修改即可。派生类可以添加自己的属性和行为,为基类提供扩展功能。
(4)多态性:指的是具有相同名称的函数方法,这意味着我们可以使用相同名称的不同方法。
• 覆盖:是运行时多态,其中的方法具有相同的名称和签名。区别在于其中一个方法在基类中,另一个方法在派生类中。通过重写,子类可以具有该方法的特定实现。
• 重载:是编译时多态,其中同一个类中有两个或多个方法具有相同的名称,但签名不同。调用哪个方法取决于所传入的值等。
2.1.5 开始Julia REPL编程
我们已经学会了如何启动Julia REPL并理解了一些基本语句,Julia提供了各种选项来运行程序。如果已经配置好了环境变量,那么可以直接运行语句,而不用打开REPL。
【范例2-2】输出Hello World
01 $ julia -e 'println("Hello World")' 02 Hello World
代码01行调用了Julia的println函数,通过-e选项来执行代码。后面使用“' '”(半角单引号)将需要执行的代码包裹起来,因为按回车键会让代码执行,所以使用这种方式来执行一些简单的代码。代码02行输出了Hello World字符串。
除此之外,还可以执行循环语句。
【范例2-3】循环输出Hello World
下面的代码通过循环语句输出了Hello World。
01 $ julia -e 'for i=1:5; println("Hello World"); end' 02 Hello World 03 Hello World 04 …
代码01行同范例2-2,同样使用了-e选项,但是在“' '”中加入了for循环语句,循环5次。由于篇幅的关系,并未给出全部代码。其输出结果是5行Hello World。
另外,还可以传递参数。
【范例2-4】传参输出
01 $ julia -e 'for i in ARGS; println(i); end' k2so r2d2 c3po r4 02 bb8 03 k2so 04 r2d2 05 c3po 06 r4 07 bb8
ARGS用于获取脚本的命令行参数。代码01行同样使用了-e选项,在该行末尾输入了一些参数,这些参数被ARGS所接收。代码03~07行为输出结果。
我们可以使用--help选项来查看Julia支持的所有选项。
01 $ julia --help 02 julia [switches] -- [programfile] [args…] 03 -v, --version 显示版本信息 04 -h, --help 显示帮助信息 05 -H, --home <dir> 设置'julia'可执行文件的位置 06 -e, --eval <expr> 评估<expr> 07 -E, --print <expr> 评估并显示<expr> 08 -L, --load <file> 立即在所有处理器上加载<file> 09 -p, --procs {N|auto} 整数值N,启动N个其他本地工作进程 10 "auto"启动的工作进程数和本地核心启动的相同 11 --machinefile <file> 在<file>中列出的主机上运行进程 12 -i 交互模式;运行REPL并且isinteractive()为真
在上面输出中,只列举了常用的一些选项。如果需要查看更多的选项,则可以像代码01行一样输入julia --help命令。