前言: 上篇文章介紹了Observable這個類的來歷。但是操作符是RxJava又一大優勢。這篇文章我會介紹一下操作符背後的相關概念。 (讀完這篇文章可能會引起身體強烈不適,甚至出現你以前懂操作符,讀了之後反而不懂的情況。甚至這篇文章對你開發Android App不會有很大幫助,所以這篇文章需要謹慎閱讀)
我們在瞭解操作符之前,首先要了解幾個概念: Monad 和 函數語言程式設計。這裡我會一一介紹他們,但是不會太詳細,一篇文章肯定不能詳細的介紹完這兩個巨大的概念,甚至我自己都沒有理解透徹這兩個概念,但是這並不妨礙我們理解RxJava的操作符。
函數語言程式設計
我們首先來說函數語言程式設計,函數語言程式設計的意義很簡單。就是 用函式來程式設計。或者說,是用數學概念上的函式( mathematical functions )來程式設計。函式是兩個集合之間的一種對映。 我們常常用 f:x -> y 這種形式來表示函式f是從X到Y的一種對映。 用我們熟悉的Kotlin語言來表示就是
fun f(x:X):Y
複製程式碼
但一般這種函式需要滿足一下幾個條件,我們才說這個函式是一個 Pure Function 也就是純函式。
- 對應一個相同的輸入值 x, 一定會獲得一個相同的輸出值 y。
- 在執行 f 的時候不會產生任何副作用。
這裡,我們又遇到了一個新名詞,副作用。我們先來看維基百科對Side Effect的解釋:
在電腦科學中,函式副作用指當呼叫函式時,除了返回函式值之外,還對主呼叫函式產生附加的影響。例如修改全域性變數(函式外的變數)或修改引數。
也就是說,任何會改變外部狀態的操作,都會被考慮為副作用,包括但不僅限於
- 對I/O的操作。例如讀取檔案或者在控制檯輸出,列印log。
- 修改外部變數,或者修改函式本身的引數。
- 丟擲異常。 等等。
Side Effect在函數語言程式設計被認為是不好的東西。因為它太不可控了,比如常用的System.currentTimeMillis()
方法。
我們每次呼叫這個方法,都會返回一個不同的值,這便是所謂的不可控。再比如readLine()
函式,我們也無法知道他究竟會讀取哪一行。
但是反過來,如果我們不是生活在“美好”的純函式世界裡。在我們的世界裡,如果沒有side effect,幾乎做不了任何事。沒有Side Effect我們甚至都不會接收到使用者輸入,因為使用者的輸入,比如螢幕點選都是一個Side Effect。為了解決這個問題,在Haskell
(一種純函數語言程式設計語言)中,引入了Monad,來控制Side Effect。
Monad
我們說Side Effect雖然是不好的,但是是有用的。我們不希望消除Side Effect,我們更希望的是Side Effect在我們掌握之中,是可控的。所以引入Monad,來控制Side Effect。 Monad 在函數語言程式設計中,有太多的教程,文章來解釋。但是看了之後都雲裡霧裡,甚至有人說過:
The curse of the monad is that once you get the epiphany, once you understand - "oh that's what it is" - you lose the ability to explain it to anybody.
Monad的詛咒就是一旦你理解他了,你就失去了向別人解釋他的能力。
我不敢說這個詛咒在我這篇文章中消除了,我只能盡我所能,用一個Android開發者讀得懂的語言盡力解釋這個概念,所以我也在前言中提到了,這篇文章讀後可能會引起嚴重不適。
So,言歸正傳,什麼是Monad。
我們回到剛才的純函式, 一個純函式比如
f : x -> y
我們如何給他加入一個可控的Side Effect? 有一種做法便是,把Side Effect統統裝進一個盒子裡,和y一起當做輸出值輸出。 比如
f : x -> S y
S 代表了在輸出y之前一系列Side Effect相關的操作。 但是這樣的問題就是,我們如果連續進行好幾個Side Effect操作。我們都要帶著這個S,比如我們有兩個函式f,g:
f : x -> S y g : y -> S z
那麼我們連續呼叫f,g之後,那結果就變成了:
f (g(x)) : x -> S(Sz)
這裡Monad就要顯示他的作用了。 很明顯,我們需要一種“組合”的能力,將兩個S結合成一個,我們更希望多個S可以結合成一個,比如這樣:
f(g(x)) : x -> S z
一個Monad 我們簡單的定義為有包含如下兩個操作的盒子S:
- 一個進入盒子的操作(Haskell中的return) return: x -> S x
在RxJava的世界中,更像是一系列產生Observable的操作符,比如
create
,just
,fromXXX
等等。比如:
val x = 10
Observable.just(x)
// 這裡我們進入了Monad的世界,而這個Monad是我們的Observable
複製程式碼
- 一個"神祕"的運算bind(haskell中的==>)。 也就是我們結合的能力,他會接收一個函式 f: x -> M y 將兩個帶有Monad的函式連在一起。
Haskell的定義: (>>=) :: m x -> ( x -> m y) -> m y
我相信大家是看不懂的,我們用Java的語言來形容一下,我們知道Java中函式不是一等公民,不能直接當引數傳給方法。我們只能用介面來模擬一個函式。 我們來定義我們的函式 function:
public interface Function<T,R>{
R apply(T t)
}
複製程式碼
T就是我們的輸入,R就是我們的輸出。(這個其實是Java 8 中的Function介面)。
而這個bind函式,就是接收一個函式f: x ->M y,然後自己生產出一個M y,我們暫時在Java世界中用Monad<X>
來代表一個Monad。
public class Monad<X> {
public Monad<Y> bind(Function<X,Monad<Y>> function)
}
複製程式碼
也就是,我們剛才所說的,結合的能力。我們通過接收一個 x -> M y 將我們的Monad<X>
轉換成了 Monad<Y>
,而不是Monad<Monad<Y>
>這樣的巢狀操作。
但其實本質上,我們得到的Monad<Y>
還是將我們本來的Monad<X>
包裹在裡面,只是形式上我們得到了Monad<Y>
。
這一部分用kotlin 可以更簡潔的表達:
class Monad<X>
fun<X,Y> Monad<X>.bind(function:(X) -> Monad<Y>) :Monad<Y>
複製程式碼
在上一篇文章中,我曾經說過
Collection可以通過高階函式(High Oroder Function)進行組合,變換等等,所以作為集合之一的Observable也可以進行組合,變換。
但是其實這句話是錯誤的,因為在上一篇文章中,我們並沒有Monad,函式式等等的知識,我們只能先這麼理解。而給予Observable這個組合,變換能力的其實就是這個Monad。 結論1 :
Observable 是一個 monad
如果入門RxJava是從RxJava1 和 扔物線大佬的給 Android 開發者的 RxJava 詳解這篇的話。 會知道RxJava 1中有一個
lift()
操作符。是幾乎所有操作符的“父”操作符,其實這也就是Monad中的bind的一個具體實現。也有人將flatMap
理解為Monad中的bind,我個人認為是不對的。他們雖然簽名是一致的,效果也是一樣的。但是flatMap操作符在RxJava中的實現和其他操作符是非常不一樣的。而lift()
在RxJava 1.x 中就擔任了所有操作符的抽象的工作。也就是我們說的接收一個 x-> Observable y 這樣一個函式,來將Observable x 轉換為 Observable y這樣一個過程。而在RxJava2 中,由於效能問題,lift()操作符實現改為了直接繼承Observable,來將lift的操作寫到subscribeActual()
來進行操作。這樣雖然減少了效能損耗,但是正確的寫一個操作符卻變得更加困難一些。
當然,不是僅僅有return 和 bind 就可以是Monad,Monad 還需要滿足如下三個規則: 這裡我們用id(X) 來代表return
-
左單位元:
id(X).bind(f:X -> Monad<Y>) = Monad<Y>
也就是bind 在左邊加上id這個函式,他獲得的還是 bind的結果Monad本身。 用RxJava 來表示就是
Observable.just(1)
.flatMap(new Function<Integer, ObservableSource<String>>() {
@Override
public ObservableSource<String> apply(Integer integer) throws Exception {
return Observable.just(integer.toString());
}
})
//這裡在just之後flatMap的observable 和我們直接使用Observable.just("1")沒有任何區別
複製程式碼
-
右單位元:
Monad(X).bind(id) = Monad<X>
也就是 如果Monad和 id 這個函式來進行結合,我們得到的還是Monad 用RxJava 來表示就是
Observable observable = Observable.just(1)
.flatMap(new Function<Integer, ObservableSource<Integer>>() {
@Override
public ObservableSource<Integer> apply(Integer integer) throws Exception {
return Observable.just(integer);
}
})
//這裡進行過 flatMap 的 observable 和我們的Observable.just(1)沒有任何區別
複製程式碼
- 結合律:
Monad<X>.bind(function :X -> Monad<Y>).bind(function:Y -> Monad<Z>)
= Monad<X>.bind(function:x -> Monad<Y>.bind(function: Y -> Monad<Z>))
複製程式碼
也就是,將後面兩個Monad,Monad合併在一起,再和Monad合併。和先合併,Monad,Monad,在與Monad合併,效果是一樣的。 用RxJava 來表示就是
Observable observable1 = Observable.just(2)
.flatMap(new Function<Integer, ObservableSource<String>>() {
@Override
public ObservableSource<String> apply(Integer integer) throws Exception {
return Observable.just(integer.toString());
}
})
.flatMap(new Function<String, ObservableSource<Double>>() {
@Override
public ObservableSource<Double> apply(String s) throws Exception {
return Observable.just(Double.valueOf(s));
}
});
Observable observable2 = Observable.just(2)
.flatMap(new Function<Integer, ObservableSource<Double>>() {
@Override
public ObservableSource<Double> apply(Integer integer) throws Exception {
return Observable.just(integer.toString())
.flatMap(new Function<String, ObservableSource<Double>>() {
@Override
public ObservableSource<Double> apply(String s) throws Exception {
return Observable.just(Double.valueOf(s));
}
});
}
});
//這裡 observable1 和 observable2 等價
複製程式碼
遵守以上三個規則,並且擁有return/id 和 bind的“盒子”,我們就稱之為一個Monad。我們在理解Monad之後,會發現我們身邊很多東西,甚至每天都在用的一些東西,他就是Monad。
比如C#中的LINQ是Monad,Java 8新引入的CompletableFuture
和Stream API是Monad, JavaScript中的Promise
是Monad,RxJava中的Observable是Monad。
這也就解釋了很多人在理解RxJava原始碼的時候,不理解為什麼 Observable 操作符要寫成這種 Observable套著Observable。最終互相通知的形式。
如:(這裡為了簡化我們使用Kotlin來寫)
Observable.just(1, 2, 3, 4)
.map{x -> x +1}
.filter { x -> x >3 }
.flatMap { x -> Observable.just(x,x+2) }
複製程式碼
這其實生成的Observable是 ObservableFlatMap(ObservableFilter(ObservableMap(ObseravbleJust(1,2,3,4))))
這樣一個一層層巢狀的Observable盒子。而賦予其巢狀能力,並將其省略為僅僅一個Observable
強大力量的便是Monad。
所以我們得出一個結論2
Observable的操作符 Monad中 bind 的一個具體實現形式。
而這個結論並不適合所有操作符,有一些特殊操作符會從Monad中跳出返回我們正常的Java/Kotlin世界。比如Subscribe
,blockingFirst()
,forEach()
等等。
這些是我們跳出Monad/Observable世界的出口。
總結: 這篇我主要介紹了函式是程式設計和Monad的概念,著重介紹了Monad和Observable緊密的關係。個人認為如果對函數語言程式設計不感興趣,對Monad的意義不必太過糾結,只需將其理解為一種對集合進行組裝變換的一種解決方案即可。