函数响应式编程(FRP)从入门到”放弃”——基础概念篇

前言

研究ReactiveCocoa一段时间了,是时候总结一下学到的一些知识了。git

一.函数响应式编程

说道函数响应式编程,就不得不提到函数式编程,它们俩到底有什么关系呢?今天咱们就详细的解析一下他们的关系。github

如今有下面4个概念,须要咱们理清一下它们之间的关系:
面向对象编程 Object Oriented Programming
响应式编程 Reactive Programming
函数式编程 Functional Programming
函数响应式编程 Functional Reactive Programming面试

咱们先来讲说什么是函数式编程Functional Programming,咱们先来看看wikipedia上的相关定义:算法

Functional Programming is a programming paradigmexpress

  1. treats computation as the evaluation of mathematical functions.
  2. avoids changing-state and mutable data

总结一下函数式编程具备如下几个特色:编程

  1. 函数是”第一等公民”
  2. 闭包和高阶函数
  3. 不改变状态(由此延伸出”引用透明”的概念)
  4. 递归
  5. 只用”表达式”,不用”语句”,没有反作用

接下来咱们依次说明一下这些特色。数组

一. 函数是”第一等公民”

所谓”第一等公民”(first class),指的是函数与其余数据类型同样,处于平等地位,能够赋值给其余变量,也能够做为参数,传入另外一个函数,或者做为别的函数的返回值。安全

一等函数的理念能够追溯到 Church 的 lambda 演算 (Church 1941; Barendregt 1984)。此后,包括 Haskell,OCaml,Standard ML,Scala 和 F# 在内的大量 (函数式) 编程语言都不一样程度地借鉴了这个概念。闭包

Ps:世界上最纯粹的函数式编程语言非Haskell莫属。并发

二.闭包和高阶函数

闭包是起函数的做用并能够像对象同样操做的对象。与此相似,函数式编程语言支持高阶函数。高阶函数能够用另外一个函数(间接地,用一个表达式) 做为其输入参数,在大多数状况下,它甚至返回一个函数做为其输出参数。这两种结构结合在一块儿使得能够用优雅的方式进行模块化编程,这是使用函数式编程的最大好处。

三. 不改变状态(由此延伸出”引用透明”的概念)

不改变状态:
函数式编程只是返回新的值,不修改系统变量。所以,不修改变量,也是它的一个重要特色。在其余类型的语言中,变量每每用来保存”状态”(state)。不修改变量,意味着状态不能保存在变量中。函数式编程使用参数保存状态,最好的例子就是递归。

避免使用程序状态和可变对象,是下降程序复杂度的有效方式之一,而这也正是函数式编程的精髓。函数式编程强调执行的结果,而非执行的过程。咱们先构建一系列简单却具备必定功能的小函数,而后再将这些函数进行组装以实现完整的逻辑和复杂的运算,这是函数式编程的基本思想。

引用透明:
若是提供一样的输入,那么函数老是返回一样的结果。就是说,表达式的值不依赖于能够改变值的全局状态。这使您能够从形式上推断程序行为,由于表达式的意义只取决于其子表达式而不是计算顺序或者其余表达式的反作用。

这里有出现了一个问题:

面试题: 纯函数式的闭包是否知足函数式编程里面不改变函数状态的特性?

根据纯函数的定义

在计算机编程中,假如知足下面这两个句子的约束,一个函数可能被描述为一个纯函数:

  1. 给出一样的参数值,该函数老是求出一样的结果。该函数结果值不依赖任何隐藏信息或程序执行处理可能改变的状态或在程序的两个不一样的执行,也不能依赖来自I/O装置的任何外部的输入(一般是这样的–看下面的描述)。
  2. 结果的求值不会促使任何可语义上可观察的反作用或输出,例如易变对象的变化或输出到I/O装置。

函数的返回值是不须要依赖全部(或任何)参数值,必须不依赖参数值之外的东西。函数可能返回多重结果值,而且对于被认为是纯函数的函数,这些条件必须应用到全部返回值。假如一个参数经过引用调用,任何内部参数变化将改变函数外部的输入参数值,它将使函数变为非纯函数。

回到咱们讨论的这个问题上来:

闭包虽然能够把闭包外部的变量捕获到闭包内部,可是闭包仍是知足不改变状态的特性的。假设f(x)的返回值是g(x),而g(x)是会依靠f(x)的参数返回的,g(x)至关于拥有f(x)的闭包。这个时候就会有一种错误的感受,g(x)捕捉了f(x)入参的变量,从而产生了不一样的闭包。从而得出g(x)不是纯函数式的,由于它改变了状态。若是咱们站在更高的层面去看待这个问题,函数在函数式编程里面是一等值,和结构体,整型,布尔类型没有区别。回到上述的问题中来,因为咱们传入了不一样参数,可是闭包里面的总体算法是没有变化的。更加详细的例子,f(x)返回一个计算x平方的函数g(x),g(x)虽然每次都会由f(x)传入的x值变化而变化,可是g(x)总体算法就是计算x的平方,这个计算方法是没有变化的,不根据外部状态改变而改变的。那么这个g(x)的block是知足函数式编程的不改变函数状态的特性的。因此它也是引用透明的。

额外须要说明的一点,__block这个关键字实际上是破坏了函数式编程的。

面试题:如何理解引用透明?

若是一个函数只会受到入参的变化,那么这个函数每次的调用都会是相同的
一个函数f(x),里面调用了g(x),g(x)里面又调用了h(x),h(x)最终计算出告终果,做为f(x)的返回值返回了。若是全部的状态都没有改变,f(x)下一次再调用相同的参数的时候,应该会获得彻底同样的结果,那这个时候其实不用再调用g(x)和h(x)了,也能够获得彻底同样的结果。当一个函数,不依赖“外部”变量和状态,只依赖入参的变化而影响函数最终返回值,也就是说入参相同,获得的返回值结果必定相同,若是函数具备这种性质,就能够说这个函数是引用透明的。

在上述例子中能够看到,若是result里面有咱们须要的值了,咱们就不会再去调用回调的闭包,这样transparent的函数每次传入相同的值,确定会返回相同的结果。

一个纯函数在执行的过程当中,只跟入参有关,在函数体中并不会引用外部全局变量,或者说是一个类方法里面的其余成员变量。另外,纯函数除了返回值以外,也不会去改变外部的变量值。知足上面这两点的纯函数,就能够说它是引用透明的。也有说法叫这种特性为幂等性

四.递归

函数式编程是用递归作为控制流程的机制。

五.只用”表达式”,不用”语句”,没有反作用

“表达式”(expression)是一个单纯的运算过程,老是有返回值;”语句”(statement)是执行某种操做,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,并且都有返回值。
缘由是函数式编程的开发动机,一开始就是为了处理运算(computation),不考虑系统的读写(I/O)。”语句”属于读写操做,因此就被排斥在外。
函数式编程强调没有”反作用”,意味着函数要保持独立,全部功能就是返回一个新的值,没有其余行为,尤为是不得修改外部变量的值。

举个例子来讲明一下函数式编程和指令式编程的区别:

上面这个例子就是计算阶乘的例子。咱们先来看看指令式编程。指令式编程,像机器一条条命令同样思考问题。指令式的思想就相似于汇编,一条条指令告诉计算机该怎么去处理这个问题。因此在指令式编程里面就有不少的状态量语句。而在函数式编程里面,思想是利用数学方法来思考问题。阶乘在数学定义里面就是f(n) = n * f(n – 1) (n > 1),f(n) = 1(n = 1)。在函数式编程里面是基本上没有状态量,只有表达式,也没有赋值语句。利用了递归解决了问题。

再来看看指令式编程和响应式编程的区别

在指令式编程里面,计算是一种瞬间的操做。而响应式编程,计算是相互相应的,相互之间都存在关系,某些变化了,相互之间的关系会使相应的值随之变化。响应式编程有2个典型的例子:Excel,当单元格变化了,相互之间的单元格也会当即变化。Autolayout,当父View变化了,根据相互之间的关系Constraint,子View的frame也会随之变化。

在面向对象语言中也是能够实现响应式编程的,具体作法应该是,把关系抽象出来,而后把变化抽象出来,用关系把变化事件传递下去。Cocoa框架下RAC的实现就是如此。

最后再来讲说函数响应式编程。
首先函数响应式编程确定是知足函数式编程的上述特性的。函数响应式编程是面向离散事件流的,在一个时间轴上会产生一些离散事件,这些事件会依次向下传递。

RAC就是Cocoa框架下的函数响应式编程的实现。它提供了基于时间变化的数据流的组合和变化。

接着再来讲说以前说的4种编程范式,总结出来,若是按照相似继承图谱来看的话,应该以下图:

a1194012-92e858ac89ee627f

 

首先在声明式编程里面有2你们族,那就是函数式编程和数据流编程,数据流编程下面就是响应式编程,而函数响应式编程是”继承”于函数式编程和响应式编程的。

1194012-b7b30d442802c2d0

 

面向对象编程就属于指令式编程的范畴。从上面2张图来看,咱们能够很明显看出这4者是什么关系了。

面试题:函数式编程是面向对象编程的升级产品
由上面的说明来看,这个说法确定是错误的,关系根据上面2图来看就很明显了。

面试题:函数式语言主张不变量的缘由是什么?

  1. 函数保持独立,全部功能就是返回一个新的值,没有其余行为,尤为是不修改外部变量的值。因为这一主张,咱们不须要考虑线程”死锁”问题,线程之间必定是安全的,由于它不修改变量,因此根本不存在”锁”线程的问题。
  2. 进一步,函数式语言更加趋向于数学公式的推导,在数学公式里面实际上是彻底不存在变量这一律念的,此时若是又不存在变量了,那整个程序的执行顺序其实就没必要要了,这样可使咱们更加容易的进行并发编程,更加有效率的利用多核cpu的计算处理能力。

二.链式调用

定义:f(x),表示的是一种态射,从x的定义域到f(x)值域的态射。若是定义域和值域是彻底相同的话,这种映射也成为单元态射。那么知足单元态射的函数,就能够进行链式调用。

以RAC为例,把RACSignal链式传递下去,subscribeNext就会返回一个RACSignal,定义域和值域都是RACSignal,那么就知足了单元态射的要求,就能够链式调用下去。

面试题:组成链式调用的必要条件就是在方法里面返回对象本身

这个说法是错误,举个例子:RAC每次作信号变换的时候,都产生了一个新的信号,因此返回本身就并非必要条件。其实若是返回本身的同类或者和本身相似的类型,里面也包含能够继续链式调用的方法,也是能够组成链式调用的。

三.关于RAC的其余一些概念

面试题:ReactiveCocoa是Facebook出的一个FRP开源库

错误,是写Github客户端时候的附属品,附带开发出的一个开源框架。

面试题:ReactiveCocoa是基于KVO的一个开源库

错误。KVO是RAC很是次要的部分,甚至能够说没有KVO,RAC依旧能够存在。

面试题:ReactiveCocoa是一个纯函数式编程的库

错误,因为Cocoa框架并非函数式,RAC又是在Cocoa框架下,因此就不是纯函数式。在命令式编程的语言范畴里面实现纯函数编程,须要折中的方法,咱们能够封装命令式编程,使其向上层能够造成纯函数式的,可是下层确定就是命令式编程实现的。

最后咱们再来区分一个概念:

面试题:RAC中Pull-driver和Push-driver的区别?

Pull-driver是指的是任什么时候刻,咱们若是须要数据了,均可以从pull-driver里面拿走数据,由于数据先存储了。整个取数据的时间控制在调用者手上。典型的例子就是for-in循环,这就是一个pull-driver的操做。无论你循环几回,每次循环如何操做,数组或者字典里面的数据都一直存在在那里,“躺”在那里。
Push-driver是相反的,在任什么时候刻,当有数据或者事件产生,都会push给你,若是你此时没有处理,该事件或者数据就丢失了。整个取数据的时间并不控制在调用者的手里。

Pull-driver能够类比看书,知识和文字无论你看不看,一直都在书里。
Push-driver能够类比看电视,节目无论你看不看,都一直播放,你错过了就是错过了。

在RAC里面,Sequence就是一个pull-driver,Signal就是一个push-driver。

未完待续……

我会不按期把关于RAC相关难理解易混淆的概念都整理进来……欢迎你们指点。

相关文章
相关标签/搜索