iThome 網站首載:FRP與函數式
在追求使用者圖型介面的流暢操作時,偶而會看到Functional Reactive Programming(FRP)的討論,其夾雜了Reactive、非同步、觀察者、資料流/事件流、函數式、宣告式等數種觀念在裏頭,加上實現時使用的語言本身語法的干擾,著實令人不易看清楚FRP的實際意義。
- Reactive Programming
暫且去掉FRP的
Functional
前置字眼,就是Reactive Programming,它一般常看到的定義是:讓資料流變化可以自動傳播的程式設計典範。在一般設計典範中,如果寫下了c=b+5
,在該程式碼運算過後,變數c
的值就固定了,若有其他流程導致變數b
改變,c
的值並不會自動變化,然而在Reactive的概念中,c
的值必須對b
的值作出反應,這對多數開發者而言,似乎是有點陌生的功能概念。實際上若用過試算表軟體,應該知道這類軟體的功能之一:可以在欄位C1輸入=B1+5,如此就會將欄位B1的值加5後作為C1的值,如果使用者變動了B1的值,那麼變化會傳播,C1的值也會自動反應變化。熟悉使用者圖型介面設計的開發者,對於MVC模式應不陌生,多個
View
可以註冊Model
,如果Model
的狀態有變化,那麼View
也會自動作出相應改變。從這個概念出發,像是聯級(Cascade)表單、搜尋框自動提示等功能,似乎都可以算是Reactive概念的實現結果。Reactive概念當然不只如此,實際上不少開發者在接觸到Reactive Programming之前,早就會使用事件等方式來組合出這類功能。例如,想讓下個輸入框反應上個輸入框的結果,只要在上個輸入框的事件發生時,在事件處理器中撰寫設定下個輸入欄文字的程式碼即可;只是若輸入框眾多,而各輸入框間的關係想讓使用者能自由指定組合時,那麼單是使用事件處理模型來解決這事,程式碼就會變得易常錯綜複雜。
Reactive的重點在於辨識出資料流,例如可以在欄位C1輸入=B1+5,然後在欄位D1輸入=C1+10,B1可以視為C1的資料來源,C1又可視為D1的資料來源,每個欄位可以與其他欄位自由組合,形成資料流延續下去;若以Reactive來處理前段述及之功能,輸入框事件必須設計成可組合,如此不但可以隨意組合輸入框事件,還可以將輸入框事件自由地與其他元件事件組合,由於事件帶有訊息等資料,因此一連串事件組合而成的事件流,也是一種資料流。
- Iterable、非同步與Observable
談到資料流處理,開發者並不陌生的就是迭代處理,開發者可以將感興趣的資料放在
Iterable
中,從中取得Iterator
進行外部迭代,在遇見感興趣資料時加以處理,像是過濾、轉換為另一串資料等。舉例來說,如果事件就是資料,那麼開發者可以將按鍵事件收集在Iterable
、將一秒內按鍵事件群組在一起、取得各群組最後一個事件、取得輸入欄的值轉換為HTTP請求等,從這點來理解,會比較容易瞭解Reactive Programming會強調資料流間銜接的概念。只不過當應用程式本身達到一定規模時,自行迭代各個資料流會是煩人之事;另一方面,
Iterable
是同步操作的概念,在某些耗時操作時,使用者就會明顯感受到頁面停止回應,在瀏覽器上,由於JavaScript執行與UI共用執行緒,問題會更加嚴重,因而必須改用非同步實現以改善此問題。先前提到MVC模式,如果Model
狀態有了變化,那麼曾訂閱過的View
也會作出相應改變,MVC實際上運用到了設計模式中的觀察者模式,在Reactive框架中也多採用此模式來實現。在API介面上,有些Reactive框架特意將
Observable
設計地與Iterable
相似,而關鍵的不同在於,Observable
是推送(Push)資料給訂閱者,而Iterable
的客戶端必須主動提取(Pull)資料。在JavaOne 2013中,Ben Christensen於〈Functional Reactive Programming with RxJava〉中舉了個對比的例子,當中getDataFromLocalMemory
執行後傳回Iterable
並執行了skip(10).take(5).map(...).forEach(...)
,而getDataFramework
後傳回的Observable
,則是skip(10).take(5).map(...).subscribe(...)
,Observable
本身可以在資料處理完成後,非同步地執行訂閱者指定的函式。- 從Functional Programming到FRP
回頭看看FRP首字F代表的Functional,這代表FRP使用Functional Programming典範來實現Reactive Programming,實際上,在Ben Christensen舉的例子中,也可以看出RxJava的
Observable
在處理資料流時,也有著函數式的味道,開發者乍遇FRP不易理解,主要在於一下子得理解資料流、非同步與Observable
等,另一方面又得理解函數式的原因。然而,如果已經熟悉函數式基本操作,那麼無論是何種資料流,在框架(或語言)支持下,就可以宣告地(Declaratively)設定資料流間的銜接,並讓感興趣者進行資料流的訂閱。舉例來說,嘗試使用傳統方式來設計簡單的滑鼠拖曳操作,必須在多種滑鼠事件發生時,使用程式碼告知電腦「如何(How)」處理事件資料,然而,如果使用Reactive Extensions,則是宣告地定義事件流、對感興趣的事件流進行訂閱。例如:
fromEvent(sprite, 'mousedown').flatMap(function() {
return fromEvent(spriteContainer, 'mousemove');
})
.takeUntil(fromEvent(spriteContainer, 'mouseup'))
.subscribe(function(dragPoint) {
sprite.style.left = dragPoint.pageX + 'px';
sprite.style.top = dragPoint.pageY + 'px';
});
如果將事件視為資料,那麼不斷發生的事件流就是資料流,對於資料流,採用函數式的高階處理,令意圖突顯出來,就是函數式的強項。像是從上例中可以看出來,滑鼠拖曳是從
mousedown
事件流開始,將每一次mousedown
轉換為mousemove
事件,直到發生mouseup
事件為止,你只要定義滑鼠拖曳是「什麼(What)」,接著mousedown
發生變化、就會一路照著定義,將變化傳播至訂閱者指定的函式,這也正是Reactive Programming自動傳播變化的概念。- 高階資料流的辨識與操作
FRP看似神秘,是因為它混合了多種概念:Reactive是其目的,也就是強調必須即時地反應變化,非同步是達到此目的之手段,為了能讓客戶端訂閱感興趣的資料流,採用了觀察者模式,為了能讓開發者不落入如何處理(事件)資料的繁雜程式邏輯中,採用了函數式的典範,隱藏了(事件)資料的迭代、轉換等細節,從而能讓開發者根據規格進行宣告,以突顯出程式本身的意圖。
其中最重要的是,開發者本身必須從需求規格中辨識出高階資料流,以及這些資料流如何使用函數式典範來定義,兩者都是多數開發者不熟悉的思考方式,然而後者實際上會是前者的基礎,因此思考訓練的起點仍是函數式,在Reactive Extension的GitHub上,有個learnx文件,就是先從函數式設計開始介紹,然後再將事件流視為資料流來介紹FRP。
實際上,無論是Functional、Reactive或是結合兩者的Functional Reactive,這些時不時跳進開發者眼裏的字眼都在傳達一件事,現今在某些場合,需要比現今開發者熟悉的命令式、物件導向等典範更高階的抽象,在將來也有可能成為經常要面對的典範,若開發者是以務實且不斷提昇作為自我期許,這將會是必修的課題!