研究ReactiveCocoa一段時間了,是時候總結一下學到的一些知識了。git
說道函數響應式編程,就不得不提到函數式編程,它們倆到底有什麼關係呢?今天咱們就詳細的解析一下他們的關係。github
如今有下面4個概念,須要咱們理清一下它們之間的關係:
面向對象編程 Object Oriented Programming
響應式編程 Reactive Programming
函數式編程 Functional Programming
函數響應式編程 Functional Reactive Programming面試
咱們先來講說什麼是函數式編程Functional Programming,咱們先來看看wikipedia上的相關定義:算法
Functional Programming is a programming paradigmexpress
- treats computation as the evaluation of mathematical functions.
- avoids changing-state and mutable data
總結一下函數式編程具備如下幾個特色:編程
接下來咱們依次說明一下這些特色。數組
所謂」第一等公民」(first class),指的是函數與其餘數據類型同樣,處於平等地位,能夠賦值給其餘變量,也能夠做爲參數,傳入另外一個函數,或者做爲別的函數的返回值。安全
一等函數的理念能夠追溯到 Church 的 lambda 演算 (Church 1941; Barendregt 1984)。此後,包括 Haskell,OCaml,Standard ML,Scala 和 F# 在內的大量 (函數式) 編程語言都不一樣程度地借鑑了這個概念。閉包
Ps:世界上最純粹的函數式編程語言非Haskell莫屬。併發
閉包是起函數的做用並能夠像對象同樣操做的對象。與此相似,函數式編程語言支持高階函數。高階函數能夠用另外一個函數(間接地,用一個表達式) 做爲其輸入參數,在大多數狀況下,它甚至返回一個函數做爲其輸出參數。這兩種結構結合在一塊兒使得能夠用優雅的方式進行模塊化編程,這是使用函數式編程的最大好處。
不改變狀態:
函數式編程只是返回新的值,不修改系統變量。所以,不修改變量,也是它的一個重要特色。在其餘類型的語言中,變量每每用來保存」狀態」(state)。不修改變量,意味着狀態不能保存在變量中。函數式編程使用參數保存狀態,最好的例子就是遞歸。
避免使用程序狀態和可變對象,是下降程序複雜度的有效方式之一,而這也正是函數式編程的精髓。函數式編程強調執行的結果,而非執行的過程。咱們先構建一系列簡單卻具備必定功能的小函數,而後再將這些函數進行組裝以實現完整的邏輯和複雜的運算,這是函數式編程的基本思想。
引用透明:
若是提供一樣的輸入,那麼函數老是返回一樣的結果。就是說,表達式的值不依賴於能夠改變值的全局狀態。這使您能夠從形式上推斷程序行爲,由於表達式的意義只取決於其子表達式而不是計算順序或者其餘表達式的反作用。
這裏有出現了一個問題:
面試題: 純函數式的閉包是否知足函數式編程裏面不改變函數狀態的特性?
根據純函數的定義
在計算機編程中,假如知足下面這兩個句子的約束,一個函數可能被描述爲一個純函數:
- 給出一樣的參數值,該函數老是求出一樣的結果。該函數結果值不依賴任何隱藏信息或程序執行處理可能改變的狀態或在程序的兩個不一樣的執行,也不能依賴來自I/O裝置的任何外部的輸入(一般是這樣的–看下面的描述)。
- 結果的求值不會促使任何可語義上可觀察的反作用或輸出,例如易變對象的變化或輸出到I/O裝置。
函數的返回值是不須要依賴全部(或任何)參數值,必須不依賴參數值之外的東西。函數可能返回多重結果值,而且對於被認爲是純函數的函數,這些條件必須應用到全部返回值。假如一個參數經過引用調用,任何內部參數變化將改變函數外部的輸入參數值,它將使函數變爲非純函數。
回到咱們討論的這個問題上來:
閉包雖然能夠把閉包外部的變量捕獲到閉包內部,可是閉包仍是知足不改變狀態的特性的。假設f(x)的返回值是g(x),而g(x)是會依靠f(x)的參數返回的,g(x)至關於擁有f(x)的閉包。這個時候就會有一種錯誤的感受,g(x)捕捉了f(x)入參的變量,從而產生了不一樣的閉包。從而得出g(x)不是純函數式的,由於它改變了狀態。若是咱們站在更高的層面去看待這個問題,函數在函數式編程裏面是一等值,和結構體,整型,布爾類型沒有區別。回到上述的問題中來,因爲咱們傳入了不一樣參數,可是閉包裏面的總體算法是沒有變化的。更加詳細的例子,f(x)返回一個計算x平方的函數g(x),g(x)雖然每次都會由f(x)傳入的x值變化而變化,可是g(x)總體算法就是計算x的平方,這個計算方法是沒有變化的,不根據外部狀態改變而改變的。那麼這個g(x)的block是知足函數式編程的不改變函數狀態的特性的。因此它也是引用透明的。
額外須要說明的一點,__block這個關鍵字實際上是破壞了函數式編程的。
面試題:如何理解引用透明?
若是一個函數只會受到入參的變化,那麼這個函數每次的調用都會是相同的
一個函數f(x),裏面調用了g(x),g(x)裏面又調用了h(x),h(x)最終計算出告終果,做爲f(x)的返回值返回了。若是全部的狀態都沒有改變,f(x)下一次再調用相同的參數的時候,應該會獲得徹底同樣的結果,那這個時候其實不用再調用g(x)和h(x)了,也能夠獲得徹底同樣的結果。當一個函數,不依賴「外部」變量和狀態,只依賴入參的變化而影響函數最終返回值,也就是說入參相同,獲得的返回值結果必定相同,若是函數具備這種性質,就能夠說這個函數是引用透明的。
1
2
3
4
5
6
7
8
9
10
11
12
|
typedef int(^intFx)(int a);
intFx transparent(intFx origin) {
NSMutableDictionary *results = [NSMutableDictionary dictionary];
return ^int(int p) {
if (results[@(p)]) {
return [results[@(p)] intValue];
}
results[@(p)] = @(origin(p));
return [results[@(p)] intValue];
};
}
|
在上述例子中能夠看到,若是result裏面有咱們須要的值了,咱們就不會再去調用回調的閉包,這樣transparent的函數每次傳入相同的值,確定會返回相同的結果。
一個純函數在執行的過程當中,只跟入參有關,在函數體中並不會引用外部全局變量,或者說是一個類方法裏面的其餘成員變量。另外,純函數除了返回值以外,也不會去改變外部的變量值。知足上面這兩點的純函數,就能夠說它是引用透明的。也有說法叫這種特性爲冪等性
函數式編程是用遞歸作爲控制流程的機制。
「表達式」(expression)是一個單純的運算過程,老是有返回值;」語句」(statement)是執行某種操做,沒有返回值。函數式編程要求,只使用表達式,不使用語句。也就是說,每一步都是單純的運算,並且都有返回值。
緣由是函數式編程的開發動機,一開始就是爲了處理運算(computation),不考慮系統的讀寫(I/O)。」語句」屬於讀寫操做,因此就被排斥在外。
函數式編程強調沒有」反作用」,意味着函數要保持獨立,全部功能就是返回一個新的值,沒有其餘行爲,尤爲是不得修改外部變量的值。
舉個例子來講明一下函數式編程和指令式編程的區別:
1
2
3
4
|
// 指令式編程
int factorial1(int x) {
int result = 1;
for (int i = 1; i
|
上面這個例子就是計算階乘的例子。咱們先來看看指令式編程。指令式編程,像機器一條條命令同樣思考問題。指令式的思想就相似於彙編,一條條指令告訴計算機該怎麼去處理這個問題。因此在指令式編程裏面就有不少的狀態量和語句。而在函數式編程裏面,思想是利用數學方法來思考問題。階乘在數學定義裏面就是f(n) = n * f(n – 1) (n > 1),f(n) = 1(n = 1)。在函數式編程裏面是基本上沒有狀態量,只有表達式,也沒有賦值語句。利用了遞歸解決了問題。
再來看看指令式編程和響應式編程的區別
1
2
3
4
5
6
7
|
void test() {
int a = 5;
int b = 8;
int c = a + b;
a = 10;
NSLog(@"%d",c);
}
|
在指令式編程裏面,計算是一種瞬間的操做。而響應式編程,計算是相互相應的,相互之間都存在關係,某些變化了,相互之間的關係會使相應的值隨之變化。響應式編程有2個典型的例子:Excel,當單元格變化了,相互之間的單元格也會當即變化。Autolayout,當父View變化了,根據相互之間的關係Constraint,子View的frame也會隨之變化。
在面嚮對象語言中也是能夠實現響應式編程的,具體作法應該是,把關係抽象出來,而後把變化抽象出來,用關係把變化事件傳遞下去。Cocoa框架下RAC的實現就是如此。
最後再來講說函數響應式編程。
首先函數響應式編程確定是知足函數式編程的上述特性的。函數響應式編程是面向離散事件流的,在一個時間軸上會產生一些離散事件,這些事件會依次向下傳遞。
RAC就是Cocoa框架下的函數響應式編程的實現。它提供了基於時間變化的數據流的組合和變化。
接着再來講說以前說的4種編程範式,總結出來,若是按照相似繼承圖譜來看的話,應該以下圖:
首先在聲明式編程裏面有2你們族,那就是函數式編程和數據流編程,數據流編程下面就是響應式編程,而函數響應式編程是」繼承」於函數式編程和響應式編程的。
面向對象編程就屬於指令式編程的範疇。從上面2張圖來看,咱們能夠很明顯看出這4者是什麼關係了。
面試題:函數式編程是面向對象編程的升級產品
由上面的說明來看,這個說法確定是錯誤的,關係根據上面2圖來看就很明顯了。
面試題:函數式語言主張不變量的緣由是什麼?
定義:f(x),表示的是一種態射,從x的定義域到f(x)值域的態射。若是定義域和值域是徹底相同的話,這種映射也成爲單元態射。那麼知足單元態射的函數,就能夠進行鏈式調用。
以RAC爲例,把RACSignal鏈式傳遞下去,subscribeNext就會返回一個RACSignal,定義域和值域都是RACSignal,那麼就知足了單元態射的要求,就能夠鏈式調用下去。
面試題:組成鏈式調用的必要條件就是在方法裏面返回對象本身
這個說法是錯誤,舉個例子:RAC每次作信號變換的時候,都產生了一個新的信號,因此返回本身就並非必要條件。其實若是返回本身的同類或者和本身相似的類型,裏面也包含能夠繼續鏈式調用的方法,也是能夠組成鏈式調用的。
面試題:ReactiveCocoa是Facebook出的一個FRP開源庫
錯誤,是寫Github客戶端時候的附屬品,附帶開發出的一個開源框架。
面試題:ReactiveCocoa是基於KVO的一個開源庫
錯誤。KVO是RAC很是次要的部分,甚至能夠說沒有KVO,RAC依舊能夠存在。
面試題:ReactiveCocoa是一個純函數式編程的庫
錯誤,因爲Cocoa框架並非函數式,RAC又是在Cocoa框架下,因此就不是純函數式。在命令式編程的語言範疇裏面實現純函數編程,須要折中的方法,咱們能夠封裝命令式編程,使其向上層能夠造成純函數式的,可是下層確定就是命令式編程實現的。
最後咱們再來區分一個概念:
面試題:RAC中Pull-driver和Push-driver的區別?
Pull-driver是指的是任什麼時候刻,咱們若是須要數據了,均可以從pull-driver裏面拿走數據,由於數據先存儲了。整個取數據的時間控制在調用者手上。典型的例子就是for-in循環,這就是一個pull-driver的操做。無論你循環幾回,每次循環如何操做,數組或者字典裏面的數據都一直存在在那裏,「躺」在那裏。
Push-driver是相反的,在任什麼時候刻,當有數據或者事件產生,都會push給你,若是你此時沒有處理,該事件或者數據就丟失了。整個取數據的時間並不控制在調用者的手裏。
Pull-driver能夠類比看書,知識和文字無論你看不看,一直都在書裏。
Push-driver能夠類比看電視,節目無論你看不看,都一直播放,你錯過了就是錯過了。
在RAC裏面,Sequence就是一個pull-driver,Signal就是一個push-driver。
我會不按期把關於RAC相關難理解易混淆的概念都整理進來……歡迎你們指點。