首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

对编程范式的简单思考

微信限制:不能放置链接代码样式比较奇怪,发布后不能更新。。。

编程最重要的事情,其实是让写出来的符号,能够简单地对实际或者想象出来的“世界”进行建模。一个程序员最重要的能力,是直觉地看见符号和现实物体之间的对应关系。 —— 王垠

为什么要写这篇文章 TL;DR

#1

一位朋友曾经曾经和我说:“千万别和不知道回调函数的人,解释什么是回调函数”。(见 如何浅显的解释回调函数)我本以为回调函数(callback function)是一个非常简单的概念,但和许多刚入门编程的人解释这个概念的时候,他们都觉得很费解。

直到现在,我才发现,原来要理解回调函数,就需要先接受 函数是一等公民(first-class function)的事实(函数和数据一样都可以被存储、传递),然后理解 高阶函数(higher-order function)的概念(函数可以作为参数传递到另一个函数里)。

这对于没有接触过函数式的人来说,简直是世界观的颠覆:

面向过程里,数据是数据、操作是操作

面向对象里,数据和操作放到对象里,属于对象的一部分

#2

为了批判面向对象里 “操作必须放到对象里” 的回调思想,写了一篇文章 回调 vs 接口(后来读到 陈硕 也有一篇类似的文章 以boost::function和boost:bind取代虚函数),但境界还不够,一直没有发现这个问题的本质——函数式 vs 面向对象。

#3

之前我也跟风写过一篇 高阶函数:消除循环和临时变量,讲的是如何使用 //// 之类的高阶函数。现在想才明白了问题的本质—— 使用函数式的方法,实现面向对象的 内部迭代(internal iteration)(属于 迭代器模式(iterator pattern)的一种),从而消除循环和迭代器临时变量。

#4

最近终于读懂了几篇 王垠的博客,大概能理解了文章的思想(虽然比较偏激,但论述非常严谨):

解密“设计模式”:批判(面向对象)设计模式(备份)

Purely functional languages and monads:批判“纯”函数式(备份)

编程的宗派:批判“纯”面向对象、“纯”函数式 和 说“各有各的好处”的“好好先生”(备份)

在《解密“设计模式”》提到,面向对象的 “设计模式” 是为了解决 “一切皆对象” 思想导致的问题。即使是 Erich Gamma(设计模式作者之一)粉丝的我,也深有感触:

由于 函数不是一等公民、不支持高阶函数,我们需要 创建型模式(creational patterns)(两种工厂、原型、创建者)

由于 不支持闭包、数据必须放到对象里,我们需要 结构型模式(structural patterns)(适配器、桥接、组合、装饰器、代理)

由于 操作必须放到对象里,我们需要 行为型模式(behavioral patterns)(命令、责任链、观察者、中介者、状态、策略、模板方法、迭代器、访问者、解释器)

Happy Coding

(by Erich Gamma)

本文总结一下自己的一些体会,并用一个例子加以阐述。

三种范式

我只接触过的三种 编程范式(programming paradigm):

面向过程(procedural programming)

面向对象(object-oriented programming)

函数式(functional programming)

面向过程

Algorithms + Data Structures = Programs(算法 + 数据结构 = 程序)—— Niklaus Wirth

数据和计算是两个不同的角色:

数据存储了程序运行的状态

计算 通过修改数据,变换运行的状态

参考:图灵机(Turing machine)

面向对象

封装(encapsulation):将数据和计算放到一起,并引入访问控制

继承(inheritance):共享数据和计算,避免冗余

多态(polymorphism):派发同一个消息(调用同一个方法),实现不同的操作(核心)

参考:浅谈面向对象编程

函数式

由于数据是有状态的(stateful),而计算是无状态的(stateless);所以需要将数据绑定(bind)到函数上,得到“有状态”的函数,即 闭包(closure)。通过构造、传递、调用 闭包,实现复杂的功能组合。

参考:λ 演算(lambda calculus)

例子:运行时动态选择计算操作

用 C++ 模拟一个简单的编辑器,点击界面上不同的按钮,执行不同的操作,并讨论各个范式如何在运行时动态选择计算操作(很简单,点击“新建”则新建文件,点击“打开”则打开文件,点击“保存”则保存文件):

新建打开保存

在程序的系统内:

点击事件的发送者是各个按钮

点击事件的接收者是程序所操作的文件

面向过程

代码链接

定义文件数据结构(用一个 封装起来):

定义文件操作(传递数据进行操作,返回 表示是否成功):

(点击事件的接收者)定义文件数据(实例化数据结构,存储程序的运行状态):

(点击事件的发送者)定义如何操作数据(跟据不同的 修改数据 ,选择不同的操作,变换程序的运行状态):

上边代码的主要问题是:事件的发送者直接依赖事件的接收者,即点击时直接用 调用 函数。这导致按钮不能复用:如果按钮点击后执行其他操作,就需要继续添加 语句。

为了让代码更灵活,我们可以使用面向对象的方法。

面向对象

代码链接

定义文件类(class)(将文件的数据和操作封装到一起,分别作为类的字段和方法;引入访问控制,隐藏数据,暴露操作;并通过异常(exception)表示是否成功):

定义命令接口 (利用 命令模式(command pattern),消除点击事件的接收者和发送者之间的依赖):

定义新建/打开/保存文件对应的实际命令 (实现 接口,执行实际的操作;存储上下文(context)数据,并在执行操作时使用):

(点击事件的接收者)定义文件对象(object)(实例化类,存储对应的数据和操作):

(在点击事件的接收者和发送者之间)定义中间层(indirection)(将点击事件的接收者 传入 的上下文;并利用 依赖注入(dependency injection)的方法,为不同的 分配不同的 ):

(点击事件的发送者)定义如何操作数据(通过中间层派发消息,利用多态机制,动态选择具体执行的操作):

在点击保存按钮时,消息的传递流程是:

命令模式最核心的地方就是:事件的发送者 发送 消息给 ,对于不同类型的 对象,会执行不同的操作,从而实现动态选择行为。这样,事件的发送者不依赖于事件的接收者 —— 按钮不关心自己被点击之后执行什么操作,照着命令去做就行。

为了解耦(decouple)事件的发送者和接收者,面向对象就引入了好几种不同的 设计行为型模式(behavioral patterns)。

这代码还可以化简吗?可以,用函数式的方法化简!

函数式

代码链接

复用上一个例子的文件类和对象(点击事件的接收者)。

重新定义 (将命令定义为一个函数):

利用新的 定义类似的中间层(将 作为上下文构造闭包,再分配给各个 ):

(点击事件的发送者)重新定义如何操作数据(通过中间层选择函数,并执行对应的函数):

和面向对象方法的区别在于:

面向对象 把数据和操作放到对象里

函数式 把数据和计算放到 lambda 里

对于 C++,上面的代码本质上是通过面向对象实现的:

是基于带有 抽象类,在构造时利用泛型技巧,抹除传入的 可调用(callable)对象的类型,仅保留调用的签名(原理 / 代码)

lambda 表达式 会被编译为带有 的类,并构造时捕获当前的上下文(类似前面的 );可以传入 封装为更抽象的可调用对象

写在最后

一切并不都是“对象”,一切也并不都是“函数”。最简单明了的,才是最好的。

有些东西本来就是有随时间变化的“状态”的,如果你偏要用“纯函数式”语言去描述它,当然你就进入了那些 monad 之类的死胡同。 ... 如果你进入另一个极端,一定要用对象来表达本来很纯的数学函数,那么你一样会把简单的问题搞复杂。 —— 王垠

本文仅是我的一些个人理解。如果有什么问题,欢迎交流。

Delivered under MIT License © 2019, BOT Man

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20190119G0MOQ100?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券