各位好,不知道各位是否有听过Functional Programming - FP,这是近期很火红的名词。
我第一次听到这个名词是一个前辈说的:「FP实在太神奇了,可恶,太晚知道FP了,真是千金难买早知道啊」,由于前辈实在很厉害,所以我当初对于FP的感受彷彿白金之星。
「我的code要飞起了吗?!」,抱着这样的心态我进一步请教他,于是他贴给了我FP的一大重点 - 「範畴学」,我一点开连结就
...
这是我当下的感受:
神人的世界就是这么朴实无华且枯燥。
FP的浅见
我个人偏向于实务主义,我常常会想FP到底实际帮助了我们什么,但许多FP文章都在探讨FP的核心,例如纯函数、惰性求值、Monad等等,这些都很酷,但换到实战中,就是不断不断的卡关,举个纯函数例子:
有个addOne的函数会对任何数字加一,即输入1的到的结果必定为2,那如果现在需要有getNumber函数去DB拿数字1,再丢去addOne函数执行,
我们要怎么确定getNumber函数去DB拿数字1是铁定拿得到的呢?
答案是不可能的,在程式的世界中,铁定有不纯的地方,我们没办法保证所有东西都纯,假设DB没有数字1,getNumber还回传1给我,那DB根本没有存在意义,FP所说的纯函数,主要是希望使用者更清楚地分开纯与不纯的界线,不让整个程式很混乱,更详细的介绍可以看良葛格的「解开对函数式的误解」
可是知道这件事后,并没有让我有得到白金之心的感觉,就是那种「恩!这的确是很正确的思想」,可是前辈所说的很神奇到底是神奇在哪@@?!
直到看到RxJS
就在有次,我遇到了一个以下的情境:
按A按钮要打一个A API,回传资料后做alert A按B按钮要打一个B API,回传资料后做alert B并且在A、B按钮都按完后,透过A与B API的资料做alert C
我们可以模拟成以下情境:
按A按钮要取得按A的滑鼠位置,回传资料后做alert A按B按钮要取得按B的滑鼠位置,回传资料后做alert B并且在A、B按钮都按完后,透过A与B的滑鼠资料做alert C
我们先来展示传统的作法:
线上运行jsfiddle
function alertC () { if (Object.keys(mouseDatas.A).length === 0 || Object.keys(mouseDatas.B).length === 0) return window.alert(`Alert C. A button x is ${mouseDatas.A.screenX}. B button x is ${mouseDatas.B.screenX}`) mouseDatas.A = {} //Reset A mouseDatas.B = {} //Reset B}const mouseDatas = { A: {}, B: {},}document.getElementById('A').addEventListener('click', mouseData => { mouseDatas.A = mouseData window.alert('Alert A') alertC()})document.getElementById('B').addEventListener('click', mouseData => { mouseDatas.B = mouseData window.alert('Alert B') alertC()})
由于A按钮与B按钮之间存在着「相依」的关係,所以我们必须生出一个global变数mouseDatas来「分享」彼此的资料
这样会导致一些问题:
这是我认为最重要的点,就是第二行的判断式子if (Object.keys(mouseDatas.A).length === 0 || Object.keys(mouseDatas.B).length === 0)
我们需要透过这个判断来确定alertC()在执行的「时间点」mouseDatas是否我们所期望的状态,就是mouseDatas的A与B都有值mouseDatas需要reset A与B,如果错的「时间点」reset或者就会导致整个流程出错整体流程的意图有些混乱,在A click的「时间点」会触发alertC函数吗?并不一定,因为他受到B click的影响。alert()这函数其实不完全属于A click或者B click,他是属于两者都被点击的状况,可读性上面我们是容易被误导的我们扩大一下情境,如果新增D~Z这些按钮,并全部按完要alertC函数,那我们全部的按钮都要添加alert()这段code大家有发现我一直强调「时间点」吗?因为我个人认为RxJS最大解决的问题就是
他明确的表达什么时间点该做什么
我们再来看看RxJS的作法:
线上运行jsfiddle
const { zip, fromEvent } = rxjs;document.getElementById('A').addEventListener('click', () => { window.alert('Alert A')})document.getElementById('B').addEventListener('click', () => { window.alert('Alert B')})// alertC的逻辑zip( fromEvent(document.getElementById('A'), 'click'), fromEvent(document.getElementById('B'), 'click')).subscribe(mouseDatas => { window.alert(`Alert C. A button x is ${mouseDatas[0].screenX}. B button x is ${mouseDatas[1].screenX}`)})
整体程式码的意思是这样的,我不用太複杂的一些专有名词来解释,而用一些口语的方式:
zip: 可以将两个event做合併fromEvent: 取得这按钮被click的eventsubscribe: 上面两个event被触发后,会执行的函数,并且也会取得这两个event的资料,会以array传入,这边以mouseDatas命名
与传统的作法做比较:
我们不需要判断时间的式子了,因为zip到subscribe这几行的意思就是「我要在两个event都执行完的时间点执行alertC」因为没有global变数,我们不需要reset,subscribe时拿到的资料是很纯粹的mouseDatas,他几乎没有可能被其他人修改,不用很害怕他是不是原本的资料还是被改过整体alertC的逻辑可读性是很好的,因为他没有穿插在A click或是B click之中,他就是一个「从zip到subscribe这几行可以表达的一个逻辑」就算我们扩大情境成有D~Z个按钮,也是单纯在zip里面添加event,不需要去修改各个按钮的click逻辑稍微统整,并与FP观念融合一下
传统做法上我们透过一个global变数来达到「判断不同时间点」的能力,但RxJS时直接透过「一行一行来表达什么时间点该做什么事」。而这一行一行的作法我们称为「流」
在对于时间的表达上有根本的不同,另外,传统做法上我们也可以依靠callback里面再塞callback来达到「有顺序」的执行,这跟时间也是有关係,但RxJS可以靠一个简单的concat来解决,连结里面的code大致如下
import { of, concat } from 'rxjs';const sourceOne = of(1, 2, 3);const sourceTwo = of(4, 5, 6);const example = concat(sourceOne, sourceTwo);const subscribe = example.subscribe(val => console.log(val));
而意思口语上就是
两者不同时间点执行但我会有顺序处理资料(event1, evnet2).处理方法subscribe(val => console.log(val))
所以我们才说RxJS提供了新思维,
RxJS提供了流的方法来处理不同时间点的资料,让我们不用透过改global变数或者callback里面叫callback这样不好「表达时间意图」的方式来处理
所以与FP观念做融合就是,我这边特别列出我自己以前的误解xd:
纯函数: 我们不採用流程外的变数来表达整个流程的目的。但不太表整个流程没有副作用,click传回来的mouseData就是副作用声明式编程很好表达流程: 我们只需透过RxJS的各个函数组合来组合click event,并把流程一行一行描述出来,这个我们称为声明式编程,而不用在A click与B click添加alert()逻辑,使得逻辑散乱,这是指令式编程的一种做法。但不代表声明式的表达能力好于指令式,比如说以下function addOne(number) { return number + 1}function getEven(number) { return number % 2 === 0}let finalNumber = 0//声明式finalNumber = [1, 2, 3, 4] .map(addOne) .filter(getEven) .reduce((accumulator, currentValue) => accumulator + currentValue)console.log(finalNumber)//指令式finalNumber = 0;[1, 2, 3, 4].forEach(number => { const addOneNumber = addOne(number) if (getEven(addOneNumber)) finalNumber = finalNumber + addOneNumber})console.log(finalNumber)
以这样的例子其实我们看不出声明式有更好表达流程,RxJS主要是透过FP的组合特性来组合各种event,所以自然而然就会变成声明式的作法,好读也是因为event变成了流的特性,但我们不能一竿子打翻一条船的说声明式一定讚惰性求值与Monad: 我们这些整个流程最后执行的方式,当然要等到event来在执行,因此惰性求值与Monad就是为了达到此目的,如果没有此想法会觉得请函数晚点执行会很不解。最后分享一下后端也是有使用RxJS情境的
RxJS通常在前端讨论,而后端较少,最根本的原因是因为后端通常就是一个requests一个response,我们较少遇到一个requets之中会有不同「时间点」的event,但我们还是有机会遇到的,
比如说: Promise要retry。我相信这是promise爱用者的一个大痛点,用传统的方法通常採用递迴,但逻辑流程会很不清楚,RxJS的作法如下:
线上运行runkit
const { from, of } = rxjs;const { switchMap, retry } = rxjs.operators;function getSearchResults(url) { return new Promise((resolve, reject) => { console.log('do again') reject("Reject") })}of("http://foo.com").pipe( switchMap(url => from(getSearchResults(url))), retry(3)).subscribe({ next: val => console.log(val), error: val => console.log(`Get error ${val}`)})
对于RxJS,我也是个新手,坦白说我主要以后端为主,所以使用到RxJS的情境没有前端那么直观,在与许多前辈与六角学院的高人讨论之后才渐渐稍有RxJS的「流式思维」,不然,我其实在FP的範畴论、Monad、functor里面打转,当然这是很重要的,但要怎么应用我以前一直不太确定。
也希望大家可以分享自己对于RxJS的想法,也欢迎指正我的文章,一起来分享这些新思考方式吧!
最近看JoJo看到完全是中毒状态
参考资料
FRP与函数式
RxJava 沉思录
希望是最浅显易懂的 RxJS 教学