为什么说面向对象编程和函数式编程

为什么说面向对象编程和函数式编程,第1张

先从结构化编程说起

很久以前,软件开发的世界还是一片浑浑噩噩,不管开发什么软件都面临着复杂性这个问题,代码里面到处是goto语句,程序的流程随意跳转。众生写代码时,越写到后面越不知道自己写的是什么。这时候出现一位巨人,它就是结构化编程。

结构化编程的基本思想是:

有序地控制流程,即把程序的执行顺序限制为顺序、分支和循环这三种;

把共通的处理归结为例程(函数)。

结构化编程的好处是:

三大限制:大大降低了程序的自由度,减少了各种组合,使得程序不至于太过复杂。对于这一点,结构化编程的顺序、分支和循环可以实现一切算法,虽然降低了程序的复杂性和灵活性,但是程序的实现能力并没有降低。

例程(函数):我们只需要知道过程(函数)的名字,而不需要知道过程的内部细节,即“黑盒化”。

虽然结构化解决了程序控制流的复杂问题,但程序里面不仅包括控制结构,还包括要处理的数据。随着处理数据的增加,程序的复杂性也会上升。这时候,面向对象编程来了!

面向对象的由来

“分别管理程序处理内容和处理数据对象所带来的复杂性”问题是,为了得到正确的结果,必须保持处理和数据的一致性,这在结构化编程中是非常困难的,解决这一问题的方案就是数据抽象技术。

数据抽象是数据和处理方法的结合。这便是最初“对象”一词的得来。面向对象编程也因此得名。

然后,从抽象原则来说,多个相同事物出现时,应该组合在一起,即DRY原则(Don't Repeat Yourself),便又引出了类这一概念。

根据数据类型来进行合适的处理(调用合适的方法),本来就应该是编程语言这种工具应该完成的事。这便是多态的引出了。

而对于继承,大部分的观点是“继承是随着程序的结构化和抽象化自然进化而来的一种方式”。结构化和抽象化,意味着把共通部分提取出来生成父类的自底向上的方法。(如果继承是这样诞生的话,那么最初,有多个父类的多重继承就会成为主流,而实际上最初引入继承的Simula语言只提供单一继承。松本行弘认为继承的原本目的实际是逐步细化)

函数响应式编程(Functional Reactive Programming:FRP)是一种和事件流有关的编程方式,其角度类似EventSoucing,关注导致状态值改变的行为事件,一系列事件组成了事件流。

FRP是更加有效率地处理事件流,而无需显式去管理状态。

具体来说,FRP包括两个核心观点:

1事件流,离散事件序列

2属性properties, 代表模型连续的值。

一系列事件是导致属性值发生变化的原因。FRP非常类似于GOF的观察者模式。

       在探讨柯里化之前,我们首先聊一聊很容易跟其混淆的另一个概念—— 偏函数(Partial Application) 。在维基百科中,对 Partial Application 的定义是这样的:

其含义是:在计算机科学中,局部应用(或偏函数应用)是指将 多个参数 固定在一个函数中,从而 产生另一个函数 的过程。

举个例子,假设我们是一个加工厂,用于生产梯形的零件,生产过程中我们要根据 订单来源方 给的一系列参数计算面积:

突然有一天,我们发现了一个问题:我们的大部分订单零件,都是高度为 28 的规格,此时面积函数调用经常是这个样子的:

此时,我们便可以 以第一个函数为模板 ,来创建 存储了固定值的新的计算函数

当然,这个示例中并没有以明显的 偏函数 的方式去呈现,我们可以让返回结果变成一个新的函数,因此我们可以加以改造:

也可以将其简化为:

这里,我们就可以将 trapezoidAreaByHeight15() 、 trapezoidAreaByHeight28() 和 trapezoidAreaByHeight33() 视为 trapezoidArea() 的偏函数。

        偏函数 往往不能改变一个函数的行为,通常是根据一个已有函数而生成一个新的函数,这个新的函数具有已有函数的相同功能,区别在于在新的函数中有一些参数 已被固定 不会变更。偏函数的设计通常:

        柯里化(Currying) 是以美国数理逻辑学家哈斯凯尔·科里(Haskell Curry)的名字命名的函数应用方式。 与偏函数很像的地方是:都可以缓存参数,都会返回一个新的函数,以提高程序中函数的适用性。 而不同点在于, 柯里化(Currying) 通常用于分解原函数式,将参数数量为 n 的一个函数,分解为参数数量为 1 的 n 个函数,并且支持连续调用。例如:

可见, 柯里化(Currying) 用于将多元任务分解成单一任务,每一个独立的任务都 缓存了上一次函数生成时传递的入参 ,并且让新生成的函数更简单、专注。上述演变也可以写作:

        柯里化(Currying) 分解了函数设计过程,将运行的步骤拆分为每一个单一参数的 lambda 演算。这里例举一个在 JavaScript 中用于做强制类型判断的示例:

使用这一的方式构建的函数 checkType() 具备了高通用性,但适用性则略差。我们发现 每次的调用过程,使用者都需要编写参数 typeStr 表示的类型字符串 ,增加了函数的应用复杂度。此时作为设计者,就可以对该函数加以改造,使其生成多个具备高适用性的独立函数:

        柯里化(Currying) 分解了函数设计过程,将运行的步骤拆分为每一个单一参数的 lambda 演算。我们可以通过递归的方式,来构造出一个可进行无限调用,并返回相同的累加函数的 柯里化函数

调用方式如:

以这样的方式,我们构建的参数是一个简单对象 nexter ,该对象至少包含一个 value 属性,用于描述本次累加的值。如果希望获取累加结果,则为 nexter 对象赋予函数属性 success 即可。结果会以实参的形式,传递给 success 函数用于传递通知。

        Promise 对象无论是构造函数还是后续的链式调用中,都能看到 柯里化 设计的影子:接收单一参数,返回一个 Promise :

调用方式为:

函数式编程或称函数程序设计,又称泛函编程,是一种编程范型,它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象。函数编程语言最重要的基础是λ演算(lambda calculus)。而且λ演算的函数可以接受函数当作输入(引数)和输出(传出值)。

比起命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。

面向对象程序设计是一种程序设计范型,同时也是一种程序开发的方法。对象指的是类的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性。

目前已经被证实的是,面向对象程序设计推广了程序的灵活性和可维护性,并且在大型项目设计中广为应用。 此外,支持者声称面向对象程序设计要比以往的做法更加便于学习,因为它能够让人们更简单地设计并维护程序,使得程序更加便于分析、设计、理解。反对者在某些领域对此予以否认。

当我们提到面向对象的时候,它不仅指一种程序设计方法。它更多意义上是一种程序开发方式。

函数式编程具有五个鲜明的特点。

1、函数是"第一等公民"

所谓"第一等公民"(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。

2、只用"表达式",不用"语句"

"表达式"(expression)是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种 *** 作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。

3、没有"副作用"

所谓"副作用"(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。

4、不修改状态

上一点已经提到,函数式编程只是返回新的值,不修改系统变量。因此,不修改变量,也是它的一个重要特点。

5、引用透明性

函数程序通常还加强引用透明性,即如果提供同样的输入,那么函数总是返回同样的结果。就是说,表达式的值不依赖于可以改变值的全局状态。

我们前篇谈了很多关于闭包的理解了,所以你应该会知道,我们现在将要谈的就是 ——异步

我们为什么觉得“异步问题”复杂呢?

其中很重要的一个原因是 —— 时间!时间将我们对数据的 *** 作、管理,变复杂了好几个量级!

(需要特别提出并明确的是: 异步和同步之间是可以相互转化的! 我们使用异步或者同步取决于 —— 如何使代码更加可读!)

函数式编程给出了实现“代码更可读”的落地原则(已多次回顾):

所以我们可以期待,异步在函数式编程中的表现!

上代码:

onCustomer() 和 onOrders() 是两个回调函数释义,两者执行的先后顺序并不能确定,所以它是一个基于时间的复杂状态。

释义:回调函数其实就是一个参数,将这个函数作为参数传到另一个函数里面,当那个函数执行完之后,再执行传进去的这个函数。

通常来说,我们最先想到的是:把 lookupOrders() 写到 onCustomer() 里面,那我们就可以确认 onOrders() 会在 onCustomer() 之后运行。

这样写,对吗?

不对!因为 onCustomer() 、 onOrders() 这两个回调函数的关系更像是一种竞争关系(都是赋值 customerorders ), 它们应该并行执行 而不是串行执行

即:我不管你们谁先执行,谁先执行完,谁就赋值给 customerorders !

那我们的思路应该是:

不过,这样让代码又变得更加难阅读!!函数内部赋值依赖于外部变量、甚至受外部回调函数的影响。

那究竟怎么办呢?

最终,我们借用 JS promise 减少这个时间状态,将异步转成同步:

两个 then() 运行之前, lookupCustomer() 和 lookupOrders() 已被同步调用,满足并行执行,谁先结束,谁赋值给 customerorders ,所以我们不需要知道谁先谁后!

在这样的实现下,不再需要时间先后的概念!减少了时间状态!!代码的可读性更高了!!

这是一个 积极的数组 ,因为它们同步(即时)地 *** 作着离散的即时值或值的列表/结构上的值。

什么意思?

a 映射到 b,再去修改 a ,b 不会收到影响。

而这,是一个 惰性的数组 , mapLazy() 本质上 “监听” 了数组 a,只要一个新的值添加到数组的末端(push()),它都会运行映射函数 v => v 2 并把改变后的值添加到数组 b 里。

什么意思?

a 映射到 b,再去修改 a ,b 也会修改。

原来,后者存在 异步 的概念。

让我们来想象这样一个数组,它不只是简单地获得值,它还是一个懒惰地接受和响应(也就是“反应”)值的数组,比如:

设置“懒惰的数组” a 的过程是异步的!

b ,是 map 映射后的数组,但更重要的是,b 是 反应性 的,我们对 b 加了一个类似监听器的东西。

这里直接给出解答:

这里再多小结一句:时间让异步更加复杂,函数式编程在异步下的运用就是减少或直接干掉时间状态。

想象下 a 还可以被绑定上一些其他的事件上,比如说用户的鼠标点击事件和键盘按键事件,服务端来的 websocket 消息等。

上述的 LazyArray 又可叫做 observable !(当然,它不止用在 map 方法中)

现在已经有各种各样的 Observables 的库类,最出名的是 RxJS Most

以 RxJS 为例:

不仅如此,RxJS 还定义了超过 100 个可以在有新值添加时才触发的方法。就像数组一样。每个 Observable 的方法都会返回一个新的 Observable,意味着他们是链式的。如果一个方法被调用,则它的返回值应该由输入的 Observable 去返回,然后触发到输出的 Observable里,否则抛弃。

比如:

本篇介绍了异步在函数式编程中的表现。

原则是:对于那些异步中有时态的 *** 作,基础的函数式编程原理就是将它们变为无时态的应用。即 减少时间状态

就像 promise 创建了一个单一的未来值,我们可以创建一个积极的列表的值来代替像惰性的observable(事件)流的值。

我们介绍了 RxJS 库,后续我们还会介绍更多优美的 JS 函数式编程库!

(俗话说的好,三方库选的好,下班都很早!!)

现在本瓜有点明白那句话了:看一门语言是不是函数式编程,取决于它的核心库是不是函数式编程。

也许我们还不熟悉像 RxJS 这类库,但我们慢慢就会越来越重视它们,越来越使用它们,越来越领会到它们!!

异步,以上。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/12156783.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-21
下一篇 2023-05-21

发表评论

登录后才能评论

评论列表(0条)

保存