我对编程范式的理解

编程范式

主要的编程范式有:

  • 命令式编程(Imperative Programming)
  • 声明式编程(Declarative Programming)
  • 函数式编程(Funational Programming)
  • 面向对象编程(Object-oriented Programming)

命令式编程

命令式编程描述代码如何工作,告诉计算机一步步地执行、先做什么后做什么。

比如,把大象装到冰箱,程序员需要做三步:

  1. 取来大象
  2. 初始化冰箱
  3. 把冰箱门打开
  4. 把大象塞进去
  5. 把冰箱门关上

举个程序的例子:

过滤一个数组,小于18的元素:

命令式会这样思考

arr = [1,21,230,44,16,10,99,2,10]

ans = []

for item in arr:
  if item < 18:
    ans.append(item)

print(ans)

主要的思考过程:

  1. 创建一个空数组,用于保存结果
  2. 然后遍历输入数组元素
  3. 对每个元素做判断,判断结果塞入空数组
  4. 迭代结束,返回数组

本质上这是一过程的描述。具体的每一步都是明确的指令。

我的看法:

维基百科上指出

命令式编程(英语:Imperative programming),是一种描述计算机所需作出的行为的编程典范。几乎所有计算机的硬件工作都是命令式的;几乎所有计算机的硬件都是设计来运行机器代码,使用命令式的风格来写的。

个人感受,更本质上,命令式,其实是底层硬件的反映。

分析上面的步骤:

  1. 创建一个空数组,用于保存结果 -> 开辟内存空间
  2. 然后遍历输入数组元素 -> CPU指针寻找元素
  3. 对每个元素做判断,判断结果塞入空数组 -> 逻辑运算单元
  4. 迭代结束,返回数组 -> 返回地址

命令式的每一步操作,都能找到对应的计算机执行部分。 更加本质的是,目前技术所有的计算机,都是冯诺依曼机,所以工作原理都十分相似。

如果有一天,出现了量子计算机,从底层工作原理发生了变化。整体的编程的思考方式也会发生变化。

声明式编程

声明式编程表明想要实现什么目的,应该做什么,但是不指定具体怎么做。

比如,把大象装冰箱问题:

  1. 取来大象
  2. 装入冰箱

过滤数组问题:

arr = [1,21,230,44,16,10,99,2,10]

def compare(x):
  return x<18

ans = filter(compare, arr)

print(ans)

对比

相比于,命令式 编程:

  • 更加简洁
  • 更加抽象
  • 更像是一种逻辑推导

compare-imperative-declarative-style

更多例子

上面还不能够完全表达出,声明式编程,更加具体的例子:

SQL就是典型的声明式程序,他关心 what 并不关心 how:

SELECT * from dogs
INNER JOIN owners

WHERE dogs.owner_id = owners.id

HTML也是

<div>
  <p>声明式example</p>
</div>

React JSX、SVG画图、Canvas、GUI 画界面等等,都是。

函数式编程、面向对象也是声明式,但是一会单独拉出去讲。

我的看法:

站在顶层去理解,声明式:

声明式编程,我认为可以简单理解是一种封装的产物。它不是说改变了工作方式,而是由于封装、逻辑预先设置,造成一种,只需要声明 what,而不需要关心how。 因为how已经被预先设置好了。

可以理解为和函数并没有什么区别。 SQL可以看做是一个复杂函数,只是看起来像语言,关键字的后面就是参数。 函数本身封装了统一的逻辑。所以,往往,我们只要 告诉他输入,还有具体的申明,就可以获得期望的返回。

这一点,自己也可以简单模仿,自己去设置一种固定的配置结构,比如JSON、Yaml。 有一个程序,固定的去读取配置。 这个程序如果是解释器。这个Conf 就像语言一般的的存在。Conf的设置,解释器都应该做出反应。

声明式是一个,更加高层的封装的结果。 仿佛就是预留参数的,明确功能的函数。

穿插讨论:面向对象

面向对象就是一种,主张 数据、行为 合在一起的封装。所以面向对象,理论上可以封装成丝毫不关心细节的声明式调用。

举个例子,面向对象一般都喜欢抽象出一个实体,然后封装属性、方法。像下面这样试用:


robotConf = {} # init config

robot = Robot(robotConf) # robot instance

print(robot.version)

robot.chatWith(me)

robot.cook(meal)

robot.do(findTom).do(sendMsg("How Are You Today")).do(order(Meal))

正如上面,我只需要关系,对象、他拥有的方法,我就可以调用。

装冰箱,面向对象举例:


elephant = Elephant()
fridge = Fridge()

fridge.add(elephant)

过滤元素的例子:

arr = GoodList([1,21,230,44,16,10,99,2,10])

def compare(x):
  return x<18

ans = arr.filter(compare, arr)

print(ans)

面向对象,就是一种视角,而这种视角,是基于面向对象的三大特性 『封装、继承、多态』,显然这三个特性,都已经被预先设计在解释器里了。 只要采用对应的字面量语法声明,就会产生效果。

最后面向对象本身,可以在这个基础上,编写出,声明式调用的程序。

函数式编程

函数式编程,是另一种视角,把执行过程堪称一系列的运算步骤。每个步骤看做是无副作用的纯函数。

命令式编程的视角,一个计算过程是这样:

a = 5 + 2;

b = a * 1;

c = b - 3;

函数式编程是这样:

result = subtract(multiply(add(1,2), 3), 4)

如果用Lisp, 前缀表达式语法是这样:

(- (* (+ 5 2) 1) 3)

面向对象的函数式风格是这样,看起来很像自然语言描述:

result = add(1,2).multiply(3).subtract(4)

这是我临时想到的

语法的表达式为了靠近一种更好的表达方式,OOP是站在自然实体映射角度,接近自然语言。 Lisp是站在数学结构角度、解释器实现角度,完美的一致。 任何风格,无非是,通过对逻辑处理的封装,形成一定的语义性。

字面量的语法形式,在程序中是被刻意『制造』出来的。它是一种多变的形式,背后可能是各种目标(可读?底层一致?足够的抽象?等等)。 归根结底,要了解的是,程序背后的本质。

不变的是冯诺依曼底层的基于存储和执行。 变化的是,中间层(可能是系统、编程语言、浏览器、解释器、一个函数 等,总是是程序构建后的),以及被设计驱动中间层的代码,想要呈现的形式, 这里有各种哲学、设计目标、对于一个事物看待的角度不同的各种尝试。

总结

这是个人的一些看法。

编程范式,不是一种金科玉律,也不是一种固定的语法,绝对的某种形态。编程范式是一种看待事物的角度。

显然编程范式会不断地变化,每种视角可以交叉印象 (只要中间层去实现)。

Mark24

Everything can Mix.