浅谈对流行编程范式FP/RP/FRP的一些理解

1,145 阅读8分钟

FP(Functional Programming)函数式编程

wiki上是这么定义函数式编程的:“一种编程范式,我们将所有计算视为纯数学函数,没有副作用, 没有任何突变。”

  • pure functions 纯函数
  • side effects free 无副作用
  • without mutations 无突变

缘起

先说fp吧,fp源于一种数学理论——Category Theory(范畴)。

范畴本意是指使用箭头连接的物体。

随便任何事物或者概念相互之间只要存在某种关系,那么它们就构成了范畴。

——《函数式编程入门教程》阮一峰

箭头表示范畴成员之间的关系,正式的名称叫做"态射"(morphism)。范畴论认为,同一个范畴的所有成员,就是不同状态的"变形"(transformation)。通过"态射",一个成员可以变形成另一个成员。

范畴如何演化到函数式编程

从范畴论的定义出发,转换到计算机的世界,范畴的成员就是变量,范畴成员之间的关系就是函数。

函数式的定义里,有三个最重要的特点:纯函数、无副作用、无突变。纯函数没问题,因为它本质上就是是数学理论派生的,尝试理解什么是副作用和突变。

定义一个函数:sum(a, b) { return a + b }

这个函数满足纯数学函数,它甚至可以被抽象到lambda演算:λ(a, b)

我们现在改写这个函数:

let result; 
sum(a, b) {    result = a + b;   return a + b; } 

这个sum函数在执行过程中,修改了外部变量。对外部产生的任何影响,抛出异常,与控制台交互,与屏幕交互,读写文件、网络、数据,包括修改变量,都被称为副作用。

再如果我们这么改:

let random; 
sum(a, b) {   return a + b + random; } 

现在这个sum函数的执行结果,被random影响。类似这种由于各种原因导致同样输入不再一定得到同样的输出的情况,被称为突变。

合成和柯里化

函数式编程有两个主要的操作,就是函数的合成和函数的柯里化。

借用一门纯函数式的语言Hashkell,来说明合成和柯里化,也许会看到不一样的世界。

简单的定义一个函数的语法:sum,求两个数字和。

Haskell中定义函数的方法就是这样,【::】读作【类型是】,规定了参数a的类型是number,sum函数的类型是a -> a -> a,最后一个是返回值(计算的结果),前面的是参数。

sum :: (Num a) => a -> a -> a 
sum a b = a + b 
sum 1 2 // result: 3 

借上面函数的执行过程,讲讲haskell里函数的柯里化,一般我们肯定会认为sum的调用就是一个函数接受两个入参,返回结果就是将两个入参相加。但在haskell或者说纯函数式的语言中不是这样的。

result = sum(1 2) 

函数的执行,乍一看可能稀松平常,就是两个入参,返回两参之和,但其实在haskell中不是这样的,这一步可以拆开来看,首先是返回中间态的珂里化函数sum1with,再执行最终的结果。

sum1with = sum 1 
result = sum1with 2 

也就能明白为啥函数定义的时候是这样的:

a -> a -> a 

而不是定义成这样:

a, a -> a 

再看函数如何合成。

一般来说,普遍接受的函数合成应该是这样的:f(g(x)),这里把g(x)的结果作为f(x)的入参来执行。

haskell使用.来组合两个函数。

首先我们再定义一个平方函数:

squa :: (Num b) => b -> b 
squa b = b * b 

现在的逻辑就是两数相加的结果再取平方:

(squa.sum)(2) // result: 16 

RP(Reactive Programming)响应式编程

一般理论的认为响应式编程就是使用异步数据流进行编程。 是一个专注于数据流和变化传递的 异步编程范式。

  • data streams 数据流:数据/事件在RP中以数据流的形式发出。
  • asynchronous 异步的:所有代码都是异步的,只需要知道结果,不需要考虑调用顺序。
  • propagate changes 传播变化:以一个数据流为输入,经过一连串操作转化为另一个数据流,并分发给各个订阅者。

数据流

任何东西都可以构造出数据流,一次网络请求,一次数据库访问,一次用户交互等等等等,数据流按照自然的时间的排序。

如最开始的那张图看到的那样,从左到右是数据流产生的顺序,通过flip变换后,会得到与之对应的结果。数据流会有三种:Value、Error和Completed,在接收到Error或者Completed之后,后续的数据流也不再接收。

传播和异步

监听一个数据流的变化被称之为「订阅(Subscribe)」,主动订阅的一方是「观察者(Observer)」,被订阅的一方也就是数据流是「被观察者(Observable)」,被观察者生产的数据流会传播给所有的观察者。

观察者对数据流的订阅,对数据流本身来说是异步,观察者不关心数据流产生的顺序和来源,只关心如何处理到达的数据。

实际编程中实践响应式编程

上面的介绍玄而又玄,我们当然能理解所谓数据流,以及数据流的传播和异步处理,那么对实际编程的帮助是什么。

这里我提出一个概念,我称它为内聚和发散的响应式。

内聚的响应式就是一段既定逻辑的响应式代码,就跟OOP中的高内聚一样,把一段逻辑高度关联的代码用响应式的方式去表达,比如下面这段代码,首先执行登陆,再分别初始化一个SDK和数据库,最后打开一个页面。

这一段代码的逻辑已经结束了,如果要新增逻辑,就只有强行在数据流的操作流程中插入逻辑。

Observable.just(context)
            .map((context)->{login(getUserId(context))})
            .map((context)->{initSDK(context)})
            .map((context)->{initDatabase(context)})
            .subscribeOn(Schedulers.newThread())
            .subscribe((context)->{startActivity()})

如果发散地完成这个逻辑,那么就应该是,我们构造了多个数据流,数据流可以被我们任意的组合和拓展。

Observable obserInitSDK=Observable.create((context)->{initSDK(context)})
                                .subscribeOn(Schedulers.newThread())

Observable obserInitDB=Observable.create((context)->{initDatabase(context)})
                                .subscribeOn(Schedulers.newThread())

Observable obserLogin=Observable.create((context)->{login(getUserId(context))})
                              .map((isLogin)->{returnContext()})
                              .subscribeOn(Schedulers.newThread())

Observable observable = Observable.merge(obserInitSDK,obserInitDB,obserLogin)

observable.subscribe(()->{startActivity()})

那么这有什么作用啊,这不就相当于是原来放在一个函数中执行,你给拆出来到多个方法吗。那么就看一个实际工程案例。

这是一个B站的直播页面,这里有直播的推流,这个直播推流的主要页面里还有弹幕、礼物动画,下面有送礼物和发弹幕的逻辑,右侧是弹幕列表区域,同样也有各种礼物动画和贵宾入参动画,在一个页面上需要请求多个接口数据,维护直播推流,弹幕的长链接。

人月神话里说过一个梗,如果原来是300个人日开发完的项目,那么请300个人是不是一天就能开发完。其实肯定不会,应该协同300个人可能是更高的成本,对于这个页面也是一样,这个页面拆分给好几个人其实需要的成本很高,界定每个人的边界,沟通每个逻辑函数的出参和入参,调用和返回的时序等等。。。

对于响应式编程来说,出参和入参都被数据流代替,调用的函数和被调用的函数就成了观察者和被观察者,函数调用的关系被改成了订阅。基于响应式的特点,函数和函数之间的关系就简化到只有数据流,同样地一个函数也不再关心另一个函数的调用和返回的时序。

FRP - 函数响应式编程

现在来到一个奇怪的世界。

首先提出一个问题,Rx*到底是RP还是FRP。

如果说它不是的话,那么在wiki的定义上是属于FRP的。

如果说它是的话,那么就连ReactX的创始人也认为Rx*不属于FRP。

下面这段来自于知乎的一个问题,仅作参考:www.zhihu.com/question/36…

FRP system就是对signals的 processors,而signals 分behaviors 和 events, 它们都是 first class values,前者连续(continously)后者不连续, behaviors 是一种time-varying values 像 time,每时每刻都在变对behavior施用函数返回behavior,这称为对函数施用的lift,可以对behavior进行shot, take, 等操作来获取(管理)状态,event不连续,直接求值无意义,但是可以通过event创建behavior, 比如hold/buffer/from event;

我自作主张去理解这里所谓的连续的behaviors和不连续的event,让我们回忆一下圆是如何求导周长的。

把矩形的角不断内折,最后正方形就几乎变成了一个圆

为什么说几乎呢,因为无论矩形的角如何去折,只要放的足够大,它永远也不会是一条真正的曲线。

圆的周长弧线就是behavior,而不断折角的矩形就是event

研究一条曲线的长度,是另一个专门的领域,叫做测度论,是用集合去定义的。

对于函数响应式的定义和实际工程应用,如果有读者比较了解,还请不吝赐教。

总结

  • FP旨在创造一个只有纯函数的世界。
  • RP将一切都视作为流。
  • 也许对FRP的一次传谣。