各位好,不知道各位是否有聽過Functional Programming - FP,這是近期很火紅的名詞。
我第一次聽到這個名詞是一個前輩說的:「FP實在太神奇了,可惡,太晚知道FP了,真是千金難買早知道啊」,由於前輩實在很厲害,所以我當初對於FP的感受彷彿白金之星。
「我的code要飛起了嗎?!」,抱著這樣的心態我進一步請教他,於是他貼給了我FP的一大重點 - 「範疇學」,我一點開連結就
...
這是我當下的感受:
神人的世界就是這麼樸實無華且枯燥。
我個人偏向於實務主義,我常常會想FP到底實際幫助了我們什麼,但許多FP文章都在探討FP的核心,例如純函數、惰性求值、Monad等等,這些都很酷,但換到實戰中,就是不斷不斷的卡關,舉個純函數例子:
有個addOne的函數會對任何數字加一,即輸入1的到的結果必定為2,那如果現在需要有getNumber函數去DB拿數字1,再丟去addOne函數執行,
我們要怎麼確定getNumber函數去DB拿數字1是鐵定拿得到的呢?
答案是不可能的,在程式的世界中,鐵定有不純的地方,我們沒辦法保證所有東西都純,假設DB沒有數字1,getNumber還回傳1給我,那DB根本沒有存在意義,FP所說的純函數,主要是希望使用者更清楚地分開純與不純的界線,不讓整個程式很混亂,更詳細的介紹可以看良葛格的「解開對函數式的誤解」
可是知道這件事後,並沒有讓我有得到白金之心的感覺,就是那種「恩!這的確是很正確的思想」,可是前輩所說的很神奇到底是神奇在哪@@?!
就在有次,我遇到了一個以下的情境:
按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都有值大家有發現我一直強調「時間點」嗎?因為我個人認為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的event
subscribe: 上面兩個event被觸發後,會執行的函數,並且也會取得這兩個event的資料,會以array傳入,這邊以mouseDatas命名
與傳統的作法做比較:
傳統做法上我們透過一個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:
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變成了流的特性,但我們不能一竿子打翻一條船的說聲明式一定讚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 教學