函數(shù)響應(yīng)式編程介紹
我今年發(fā)布了一個(gè)關(guān)于函數(shù)響應(yīng)式編程(Funcational Reactive Programming 簡稱FRP)的演講毕泌,試圖分解給它的名字還有你為什么要關(guān)心它的名字叠赦。這是關(guān)于那次演講的文章。
介紹
在過去幾年中搁料,函數(shù)響應(yīng)式編程非常流行熔脂。但是為什么是它融虽?你為什么需要關(guān)心眉睹。
即使是對(duì)于使用像RxJava
這類響應(yīng)式框架的人荔茬,F(xiàn)RP背后的根本原因也可能是神秘的废膘。今天要打破這份神秘,把它分解成單獨(dú)的組件:響應(yīng)編程和函數(shù)編程慕蔚。
響應(yīng)編程
首先丐黄,讓我們來看看響應(yīng)代碼的意義。
我們從一個(gè)簡單的例子開始:一個(gè)開關(guān)和一個(gè)燈泡孔飒。當(dāng)你滑動(dòng)開關(guān)灌闺,燈泡打開或者關(guān)閉。
編碼方面坏瞄,兩個(gè)組件是耦合的桂对。通常你不會(huì)關(guān)心它們是怎么耦合的,但是讓我們深入挖掘惦积。
一種方法是讓開關(guān)修改燈泡的狀態(tài)。這種情況下猛频,開關(guān)是主動(dòng)的狮崩,將新的狀態(tài)推給燈泡;而燈泡是被動(dòng)的鹿寻,僅僅接收指令去改變它的狀態(tài)睦柴。
我們將通過在開關(guān)上放一個(gè)箭頭來表示這段關(guān)系——也就是說,連接兩個(gè)元件的是開關(guān)毡熏,不是燈泡坦敌。
這有一個(gè)主動(dòng)解決方案的草圖:這個(gè) Switch
包含一個(gè)LightBulb
實(shí)例,當(dāng)它狀態(tài)發(fā)生變化時(shí)它會(huì)修改痢法。
另一種對(duì)這些組件進(jìn)行耦合的方法是讓燈泡監(jiān)聽開關(guān)的狀態(tài)然后響應(yīng)地修改自己狱窘。在這個(gè)模型中,燈泡是響應(yīng)的财搁,根據(jù)開關(guān)的狀態(tài)改變它的狀態(tài)蘸炸;而開關(guān)是可觀察的,其他的是可以觀察到他的狀態(tài)改變尖奔。
這里是被動(dòng)解決方案的一個(gè)示意圖:LightBulb
接收到一個(gè) Switch
的監(jiān)聽事件,然后根據(jù)監(jiān)聽修改它自己的狀態(tài)。
最終對(duì)于用戶磨取,主動(dòng)和被動(dòng)的代碼都導(dǎo)致了相同的結(jié)果殿如。兩種方法有什么不同之處呢?
第一個(gè)區(qū)別是誰控制燈泡茴扁。在主動(dòng)模型中铃岔,它必須是調(diào)用LightBulb.power()
的額外組件。燈泡具有響應(yīng)性峭火,燈泡自己控制它的亮度德撬。
第二個(gè)區(qū)別是誰決定Switch
的控制铲咨。在主動(dòng)模型中,Swtich
自己決定了它的控制是誰蜓洪。在響應(yīng)模型中纤勒,Switch
不知道他的驅(qū)動(dòng)是什么,因?yàn)槠渌M件通過監(jiān)聽接入它隆檀。
你會(huì)覺得這兩種模型互為鏡像摇天。主動(dòng)和被動(dòng)的編碼之間有一種二重性。
然而恐仑,兩個(gè)組件的耦合緊密或者松散還有有一個(gè)細(xì)微的差別泉坐。在主動(dòng)模型中,模塊直接相互控制裳仆。在響應(yīng)模型中腕让,模塊控制他們自己,間接的相互連接歧斟。
讓我們來看看這是怎么樣在現(xiàn)實(shí)生活例子中體現(xiàn)出來的纯丸。這是Trello的主屏幕。它顯示了你從數(shù)據(jù)庫中得到的板静袖。如何體現(xiàn)和主動(dòng)或被動(dòng)模型的關(guān)系呢觉鼻?
使用主動(dòng)模型,當(dāng)數(shù)據(jù)庫變化時(shí)队橙,它將這些改變推給 UI坠陈。但是這沒有任何意義:為什么我的數(shù)據(jù)庫要關(guān)心UI?為什么它必須檢查主屏幕是否正在顯示捐康,并知道是否應(yīng)該推新數(shù)據(jù)給它仇矾。主動(dòng)模型在DB和UI之間創(chuàng)建了一個(gè)奇怪的緊密耦合。
相比之下解总,響應(yīng)式模型干凈很多∪粑矗現(xiàn)在我的UI監(jiān)聽數(shù)據(jù)的變化,有必要的時(shí)候更新自己倾鲫。數(shù)據(jù)庫只是提供一個(gè)監(jiān)聽的不說話的知識(shí)庫粗合。誰都能更新它,這些改變僅僅反映在需要的UI中乌昔。
這是好萊塢的原則:別大電弧給我們隙疚,我們會(huì)打電話給你。這對(duì)松散耦合代碼來說是很好的磕道,允許你封裝你的組件供屉。
我們現(xiàn)在能回答什么是 響應(yīng)式編程:當(dāng)你首先關(guān)注響應(yīng)式代碼,代替你默認(rèn)的主動(dòng)代碼。
如果你想默認(rèn)為響應(yīng)式伶丐,我的的簡單的監(jiān)聽就不是很好了悼做。它有以下幾個(gè)問題:
第一,每個(gè)監(jiān)聽是獨(dú)一無二的哗魂。我們有 Switch.OnFlipListener
肛走,但是只適用于 Switch
÷急穑可以觀察到的每個(gè)模塊都必須實(shí)現(xiàn)它自己的偵聽器設(shè)置朽色。這不僅意味著成串的額外樣板代碼,還意味著你不能重用響應(yīng)模式组题,因?yàn)闃?gòu)建通用的框架葫男。
第二個(gè)問題是每個(gè)觀察者對(duì)可觀察組件有直接使用權(quán)。LightBulb
必須有直接的 Switch
才能開始監(jiān)聽它崔列。這導(dǎo)致模塊之間的緊密耦合梢褐,破壞了我們的目標(biāo)。
我們真正想要的是赵讯,如果 Switch
.flips()返回一些可以傳遞的通用類型盈咳。我們來看看能返回什么類型,滿足我們的需求瘦癌。
函數(shù)可以返回四個(gè)基本對(duì)象猪贪。子啊一個(gè)軸上返回item的數(shù)量:單個(gè)項(xiàng)或者多個(gè)項(xiàng)跷敬。另一個(gè)軸讯私,表示item是否立馬返回(sync),或者表示項(xiàng)是否稍后將交付的值(async)西傀。
同步返回很簡單斤寇。單個(gè)返回類型為T:任何類型。同樣的拥褂,多個(gè)項(xiàng)只是一個(gè)Iterable<T>
娘锁。
使用同步代碼進(jìn)行編程很簡單,因?yàn)楫?dāng)你獲得它們時(shí)饺鹃,你能開始使用返回值莫秆,但是我們不在這個(gè)世界中。響應(yīng)式編碼被指上市異步的:無法知道可觀察組件何時(shí)會(huì)發(fā)出一個(gè)新的狀態(tài)悔详。
因此镊屎,讓我們研究一下異步返回。一個(gè)單一的異步項(xiàng)相當(dāng)于Future<T>
茄螃。很好缝驳,但不是我們想要的——一個(gè)可觀察組件可能有多項(xiàng)(例如: Switch
可以打開/關(guān)閉很多項(xiàng))。
我們真正想要的是右下角。我們稱上一象限是一個(gè)Observable<T>
用狱。一個(gè)Observable
是所有響應(yīng)式框架的基礎(chǔ)运怖。
讓我們看看Observable<T>
是怎么工作的。在我們的新代碼中夏伊, Switch
.flips()返回一個(gè)Observable<Boolean>
——即一個(gè)表示 Switch
狀態(tài)的true/false的序列∫≌梗現(xiàn)在我們的LightBulb
,不是直接消費(fèi) Switch
署海,將會(huì)訂閱 Switch
提供的一個(gè)Observable<Boolean>
吗购。
這段代碼的表現(xiàn)和我們沒有Observable
的代碼是一樣的,但是修復(fù)了我前面提到的兩個(gè)問題砸狞。Observable<T>
是一個(gè)廣義的類型捻勉,允許我們?cè)谒厦娼ⅰK梢员粋鬟f刀森,所以我們的組件也不是緊密耦合的了踱启。
讓我們鞏固一下Observable
的基礎(chǔ)知識(shí)。Observable
是隨著時(shí)間推移的item的集合研底。
這是我展示的一個(gè)彈珠圖埠偿。這條線表示時(shí)間,而圓表示的是Observable
推給訂閱者的事件榜晦。
Observable
可能導(dǎo)致兩種可能終端狀態(tài)之一:成功的完成和錯(cuò)誤冠蒋。
一個(gè)成功完成彈珠圖中的一條垂直線表示。不是所有集合都是無限的乾胶,也有必要能夠表示它抖剿。例如,你在Netflix
放一個(gè)視頻识窿,視頻在某個(gè)時(shí)候會(huì)結(jié)束斩郎。
一個(gè)錯(cuò)誤是由X表示,是由于某種原因?qū)е聰?shù)據(jù)流的結(jié)果無效喻频。例如缩宜,有人拿錘子砸我們的開關(guān),那就值得告訴大家我們的開關(guān)不僅停止發(fā)出任何新的狀態(tài)而且也不能有效的監(jiān)聽更多甥温,因?yàn)樗呀?jīng)壞了锻煌。
函數(shù)式編程
讓我們先把響應(yīng)式編程放到一邊,然后跳到什么是函數(shù)式編程姻蚓。
函數(shù)式編程關(guān)注函數(shù)宋梧。對(duì)吧?我不是說任何簡單的老函數(shù):我們用純函數(shù)來教學(xué)史简。
讓我通過反例解釋下什么是純函數(shù)乃秀。
假設(shè)我們有一個(gè)完全合理的add()
函數(shù)肛著,它將兩個(gè)數(shù)加在一起。但是等等跺讯,函數(shù)中所有的空空間是什么枢贿?
哦!看起來像add()
發(fā)送文本到控制臺(tái)刀脏。這就是所謂的副作用局荚。add()
的目的不是打印到控制臺(tái);它是兩個(gè)數(shù)相加愈污。然而它正在修改應(yīng)用程序的全局的狀態(tài)耀态。
但是等等,還有更多。
哎喲暂雹!它不僅打印到控制臺(tái)首装,還殺死程序。如果你只是看函數(shù)定義(兩個(gè)int輸入杭跪,一個(gè)int輸出),你就不會(huì)知道使用這個(gè)方法會(huì)對(duì)你的應(yīng)用程序造成什么樣的毀壞仙逻。
讓我們看另一個(gè)例子。
這里涧尿,我們?nèi)∫粋€(gè)列表看看所有元素的和和乘積是否一樣系奉。我認(rèn)為這對(duì)[1,2,3]是成立的,因?yàn)?+2+3==6和1*2*3==6.
然而姑廉,檢查sum()
方法是如何實(shí)現(xiàn)缺亮。它不會(huì)影響我們app全局的狀態(tài),但是它修改我們的輸入桥言!這意味著代碼將會(huì)失敗萌踱,因?yàn)樵?code>product(numbers)執(zhí)行的時(shí)候,numbers
是空的限书。像天方夜譚虫蝶,這類問題會(huì)一直出現(xiàn)在實(shí)際的不純的函數(shù)中章咧。
任何時(shí)候你在函數(shù)外面改變狀態(tài)都會(huì)產(chǎn)生副作用倦西。正如你所看到的,副作用可能使寫碼變的困難赁严。純函數(shù)不允許有任何的副作用扰柠。
有趣,這意味著純函數(shù)必須返回一個(gè)值疼约。返回類型是void的純函數(shù)什么都做不了卤档,因?yàn)椴荒苄薷妮斎牖蛘呷魏魏瘮?shù)外的狀態(tài)。
它也意味著你的函數(shù)輸入必須是不可變的程剥。我們不允許輸入可變劝枣,否則當(dāng)一個(gè)函數(shù)執(zhí)行時(shí)并發(fā)代碼可能改變輸入,打破了純潔性。順便說下舔腾,這也意味著輸出也應(yīng)該是不可變的(否則他們不能作為純函數(shù)的輸入)溪胶。
純函數(shù)有第二個(gè)方面,給相同的輸入稳诚,它們必須總是返回相同的輸出哗脖。換句話說,它們布恩那個(gè)依賴與任何函數(shù)外部的狀態(tài)扳还。
例如才避,檢查和用戶打招呼的函數(shù)。它是沒有任何副作用的氨距,但是它隨機(jī)返回兩個(gè)問候中的一個(gè)桑逝。隨即形式通過一個(gè)外部的靜態(tài)函數(shù)還提供的。
這讓寫碼更加困難俏让,有兩個(gè)原因肢娘。首先,不管的輸入該方法有不一致的結(jié)果舆驶。如果你知道給函數(shù)相同的輸入結(jié)果是相同的輸出橱健,那寫碼就更容易了。其次沙廉,你現(xiàn)在有一個(gè)有外部依賴項(xiàng)的行數(shù)拘荡;如果外部依賴被任何方式改變,函數(shù)運(yùn)行的表現(xiàn)可能會(huì)不同撬陵。
對(duì)于面向?qū)ο箝_發(fā)者來說是困惑的珊皿,這意味著純函數(shù)甚至不能訪問它包含在其中的類的狀態(tài)。例如:Random的方法本質(zhì)是不純的巨税,因?yàn)檎{(diào)用它們根據(jù)Random的內(nèi)部狀態(tài)返回新的值蟋定。
簡單地說,函數(shù)式編程是基于純函數(shù)的草添。純函數(shù)是一個(gè)不能消費(fèi)或者改變外部狀態(tài)的函數(shù)——他們完全依賴于輸入來獲取輸出驶兜。
有一點(diǎn)困惑,經(jīng)常碰到人介紹FP:你是如何改變的远寸?例如抄淑,如果我想要獲取一個(gè)整數(shù)列表并將它們的所有值都加倍?當(dāng)然你必須改變列表,對(duì)吧?
嗯驰后,不完全是肆资。你能用純函數(shù)轉(zhuǎn)換你的列表。這是一個(gè)使列表中的值加倍的純函數(shù)灶芝。沒有副作用郑原,沒有外部狀態(tài)唉韭,也沒有輸入/輸出的改變。這個(gè)函數(shù)提供了改變工作犯犁,所以你不必做纽哥。
然而,我們寫的函數(shù)靈活度不高栖秕。它能做是將數(shù)組中每個(gè)數(shù)加倍春塌,但是我能想象我們對(duì)一個(gè)整型數(shù)組做更多的操作:所有值三倍,所有值減半...想法是無限的簇捍。
讓我們寫一個(gè)通用的整數(shù)操作器只壳。我們從一個(gè)Function
接口開始,這允許我們定義我們想要的如何才做每個(gè)整數(shù)暑塑。
然后吼句,我們將寫一個(gè)map()行數(shù),同時(shí)接收整數(shù)數(shù)組和一個(gè)Function
事格。每一個(gè)數(shù)組中的整數(shù)惕艳,我們都能應(yīng)用Function
。
瞧!有了一些額外的代碼驹愚,我們現(xiàn)在可以將任何整數(shù)數(shù)組映射到另一個(gè)整數(shù)數(shù)組远搪。
我們更深入的看這個(gè)例子:為什么不使用泛型,這樣我們對(duì)于任何列表都可以從一種類型轉(zhuǎn)換為另一種類型逢捺。修改之前的代碼不是那么難谁鳍。
現(xiàn)在,我們可以將任何List<T>
映射到List<R>
中劫瞳。例如倘潜,我們能獲取一個(gè)字符串列表轉(zhuǎn)換為每個(gè)字符串長度的列表。
我們的map()被稱為高階函數(shù)志于,因?yàn)樗邮找粋€(gè)函數(shù)作為參數(shù)涮因。能夠傳遞和使用函數(shù)的去昂達(dá)工具因?yàn)樗试S代碼更加靈活。而不是寫重復(fù)的伺绽、特定的函數(shù)养泡,你可以寫像map()這樣的通用函數(shù),這函數(shù)很多情況下都是可重用的憔恳。
除了由于沒有外部狀態(tài)工作更容易以外瓤荔,純函數(shù)也讓編寫函數(shù)更容易净蚤。如果你一個(gè)函數(shù)是A->B而另一個(gè)是B->C钥组,那么我們能把這兩個(gè)函數(shù)結(jié)合起來創(chuàng)建一個(gè)A->C。
當(dāng)你便攜不純函數(shù)的時(shí)候今瀑,經(jīng)常出現(xiàn)不想要的副作用程梦,這意味著很難知道組合函數(shù)是否能正常工作点把。只有純函數(shù)能保證程序員的組合是安全的。
讓我們來看一個(gè)合成的例子屿附。這是另一個(gè)通常的FP函數(shù)——filter()郎逃。他使我們所有列表中的項(xiàng)。現(xiàn)在挺份,我們可以通過組合兩個(gè)函數(shù)在轉(zhuǎn)換之前過濾我們的列表褒翰。
我們現(xiàn)在有了一對(duì)小但是功能強(qiáng)大的轉(zhuǎn)換函數(shù),它們通過允許我們組合它們來增強(qiáng)它們的功能匀泊。
函數(shù)式編程的內(nèi)容遠(yuǎn)比我這里介紹的多优训,但這一速成課程足以理解“FRP”部分的“FP”部分。
函數(shù)響應(yīng)式編程
讓我們看看函數(shù)式編程如何增強(qiáng)響應(yīng)式代碼各聘。
假設(shè)我們的 Switch
揣非,提供的不是Observable<Boolean>
,而是提供一個(gè)有枚舉基礎(chǔ)的Observable<State>
躲因。
現(xiàn)在表面上看來我們沒法 Switch
換LightBulb
因?yàn)槲覀儾患嫒莘盒驮缇础5怯幸粋€(gè)很明顯的方法Observable<State>
模仿Observable<Boolean>
——如果我們能將一種類型的流轉(zhuǎn)換成另一種類型,那怎么做大脉?
還記得我們?cè)贔P中看到的map()
函數(shù)么搞监?它將一個(gè)類型的同步集合轉(zhuǎn)換成另一種類型。如果我們將相同的想法應(yīng)用到一個(gè)異步集合上呢镰矿?像Observable
腺逛。
Ta-da:這是map()
,但是是為了Observable
。Observable.map()
是所謂的操作符衡怀。操作符讓你以任何方式轉(zhuǎn)換一個(gè)Observable
流棍矛。
這是一個(gè)操作符的彈珠圖,比我們之前看到的要復(fù)雜抛杨。讓我們來分解一下:
上面的一行表示輸入流:一系列彩色的圓圈够委。
中間框表示操作符:將圓形轉(zhuǎn)換為正方形。
下面的一行表示輸出流:一系列彩色的方塊怖现。
本質(zhì)上茁帽,它是輸入流中的每個(gè)條目的1:1轉(zhuǎn)換。
讓我們把它應(yīng)用到我們的開關(guān)問題屈嗤。我們從Observable<State>
開始潘拨。然后我們用map()
這樣每當(dāng)一個(gè)新狀態(tài)被釋放時(shí),它就會(huì)被轉(zhuǎn)換為一個(gè)布爾值饶号;因此map()返回Observable<Boolean>
√罚現(xiàn)在我們有了正確的類型,我們能構(gòu)造我們的LightBulb
了茫船。
好了琅束,這就是有用的扭屁。但是這和純函數(shù)有什么關(guān)系呢?你不能在map()內(nèi)部寫任何東西涩禀,包括副作用料滥?當(dāng)然性穿,你可以...但是疾嗅,你的代碼很難和它一起工作竭钝。另外肩钠,你錯(cuò)過了一些無副作用的操作符組合牡彻。
想象我們的State枚舉
在兩個(gè)以上吴裤,但是我們只關(guān)心開啟/關(guān)閉狀態(tài)突硝。這種情況下设拟,我我們希望過濾掉中間的狀態(tài)雁社≡【看,在FRP中還有一個(gè)filter()
操作符霉撵;我們可以和map()
組合起來獲得我們想要的結(jié)果磺浙。
如果你將FRP的代碼和FP的代碼進(jìn)行對(duì)比,你會(huì)看到相似之處徒坡。唯一的區(qū)別就是FP代碼是處理同步集合撕氧,而FRP菜嗎處理的是異步集合。
FRP中有大量的運(yùn)算符喇完,覆蓋了許多用于流處理的常見情況伦泥,這些操作可以應(yīng)用和組合在一起。讓我們來看一個(gè)真實(shí)的例子锦溪。
我之前展示的Trello主屏幕非常簡單——它有一個(gè)從數(shù)據(jù)庫到UI的大箭頭不脯。但實(shí)際上,我們的主屏幕使用了大量的數(shù)據(jù)來源刻诊。
特別是防楷,我們有團(tuán)隊(duì)的來源,每個(gè)團(tuán)隊(duì)內(nèi)部都有多個(gè)董事會(huì)则涯。我們要確保我們能同步接收到這些數(shù)據(jù);我們不希望有不匹配的數(shù)據(jù)复局,比如沒有它的父團(tuán)隊(duì)的董事會(huì)。
為了解決這個(gè)問題粟判,我們可以使用combineLatest()
操作符亿昏,它接收多個(gè)流并將他們組合成一個(gè)復(fù)合流。特別有用的档礁,每次它的輸入流更新時(shí)它也會(huì)更新角钩,因此我們可以確保我們發(fā)送到UI
的數(shù)據(jù)是完整的、最新的。
在FRP方面確實(shí)有大量的操作符彤断。這里有幾個(gè)有用的...通常野舶,當(dāng)人們第一次接觸到FRP時(shí)易迹,他們看到操作符的列表就頭暈了宰衙。
然而這些操作符的目的不是要壓倒一切,而是在應(yīng)用程序中對(duì)典型的數(shù)據(jù)流驚醒建模睹欲。他們是你的朋友供炼,而不是你的敵人。
我的建議是窘疮,每次都要一步一步來袋哼。不要一下子記住所有的元素安撫,相反闸衫,只需要意識(shí)到一個(gè)操作符可能已經(jīng)存在于你想要做的事情中涛贯。當(dāng)你需要的時(shí)候去找他們,再練習(xí)之后你會(huì)習(xí)慣的蔚出。
題外話
我試著回答“什么是函數(shù)響應(yīng)式編程?”“我們現(xiàn)在有了一個(gè)答案:它是響應(yīng)式的流弟翘,結(jié)合了函數(shù)運(yùn)算符。
但是你為什么要嘗試使用FRP呢?
反應(yīng)流允許您通過標(biāo)準(zhǔn)化組件間通信的方法來編寫模塊化代碼骄酗∠∮啵活性數(shù)據(jù)流也允許這些組件之間的松散耦合。
響應(yīng)流本身也是異步的趋翻。也許您的工作完全是同步的睛琳,但是我所使用的大多數(shù)應(yīng)用程序都依賴于異步用戶輸入和并發(fā)操作。使用異步代碼設(shè)計(jì)的框架比嘗試編寫自己的并發(fā)解決方案更容易踏烙。
FRP的函數(shù)部件是有用的师骗,因?yàn)樗茏屇阋砸环N合理的方式處理流。函數(shù)操作符允許你控制流的交互方式讨惩。它還提供了用于復(fù)制一個(gè)邏輯的通用邏輯工具丧凤。
函數(shù)響應(yīng)式編程不是很直觀。大多數(shù)人都是主動(dòng)和不純的編碼步脓,包括自己愿待。你做的時(shí)間足夠長,你的腦海會(huì)開始固化靴患,不純的編碼示威的解決方案仍侥。打破這種思維模式可以讓你通過函數(shù)響應(yīng)式編程編寫更有效的代碼。
資源
我想感謝/指出我為這次演講所做的一些資源鸳君。
- cycle. js對(duì)主動(dòng)與被動(dòng)代碼有很好的解釋农渊,這是我在這次演講中大量借用的。
- Erik Meijer對(duì)主動(dòng)/被動(dòng)性的二元性進(jìn)行了精彩的討論或颊。我從這里借用了函數(shù)的四個(gè)基本效應(yīng)砸紊。這個(gè)演講很有數(shù)學(xué)意義传于,但如果你能通過它,它會(huì)很有啟發(fā)性醉顽。
- 如果您想要更多地了解函數(shù)式編程沼溜,我建議您嘗試使用一種實(shí)際的FP語言。Haskell尤其具有啟發(fā)性游添,因?yàn)樗鼑?yán)格遵守外交政策系草,這意味著你不能欺騙你的實(shí)際學(xué)習(xí)。如果你想進(jìn)一步調(diào)查唆涝,“學(xué)Haskell”是一本不錯(cuò)的免費(fèi)在線書找都。
- 如果你想了解更多關(guān)于FRP的知識(shí),請(qǐng)參閱我自己的系列博客文章廊酣。這些文章中涉及到的一些主題都是在這里討論的能耻,所以讀這兩篇文章可能會(huì)有點(diǎn)重復(fù),但是它會(huì)詳細(xì)介紹RxJava的更多細(xì)節(jié)亡驰。