一篇不太一樣的RxJava介紹(二):關于操作符背后的故事

前言: 上篇文章介紹了Observable這個類的來歷。但是操作符是RxJava又一大優勢。這篇文章我會介紹一下操作符背后的相關概念。
(讀完這篇文章可能會引起身體強烈不適,甚至出現你以前懂操作符,讀了之后反而不懂的情況。甚至這篇文章對你開發Android App不會有很大幫助,所以這篇文章需要謹慎閱讀)

我們在了解操作符之前,首先要了解幾個概念: Monad函數式編程。這里我會一一介紹他們,但是不會太詳細,一篇文章肯定不能詳細的介紹完這兩個巨大的概念,甚至我自己都沒有理解透徹這兩個概念,但是這并不妨礙我們理解RxJava的操作符。

函數式編程

我們首先來說函數式編程,函數式編程的意義很簡單。就是 用函數來編程。或者說,是用數學概念上的函數( mathematical functions )來編程。函數是兩個集合之間的一種映射。
我們常常用 f:x -> y 這種形式來表示函數f是從X到Y的一種映射。
用我們熟悉的Kotlin語言來表示就是

    fun f(x:X):Y

但一般這種函數需要滿足一下幾個條件,我們才說這個函數是一個 Pure Function 也就是純函數。

  1. 對應一個相同的輸入值 x, 一定會獲得一個相同的輸出值 y。
  2. 在執行 f 的時候不會產生任何副作用

這里,我們又遇到了一個新名詞,副作用。我們先來看維基百科對Side Effect的解釋:

在計算機科學中,函數副作用指當調用函數時,除了返回函數值之外,還對主調用函數產生附加的影響。例如修改全局變量(函數外的變量)或修改參數。

也就是說,任何會改變外部狀態的操作,都會被考慮為副作用,包括但不僅限于

  1. 對I/O的操作。例如讀取文件或者在控制臺輸出,打印log。
  2. 修改外部變量,或者修改函數本身的參數。
  3. 拋出異常。
    等等。

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:

  1. 一個進入盒子的操作(Haskell中的return) return: x -> S x
    在RxJava的世界中,更像是一系列產生Observable的操作符,比如create,just,fromXXX等等。比如:
    val x = 10
    Observable.just(x)
    // 這里我們進入了Monad的世界,而這個Monad是我們的Observable
  1. 一個"神秘"的運算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

  1. 左單位元:

    id(X).bind(f:X -> Monad<Y>) = Monad<Y>

    也就是bind 在左邊加上id這個函數,他獲得的還是 bind的結果Monad<Y>本身。
    用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")沒有任何區別

  1. 右單位元:

    Monad(X).bind(id) = Monad<X>

    也就是 如果Monad<X>和 id 這個函數來進行結合,我們得到的還是Monad<X>
    用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)沒有任何區別
  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<Y>,Monad<Z>合并在一起,再和Monad<X>合并。和先合并,Monad<X>,Monad<Y>,在與Monad<Z>合并,效果是一樣的。
用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的意義不必太過糾結,只需將其理解為一種對集合進行組裝變換的一種解決方案即可。

參考文獻(部分鏈接可能需要梯子)

  1. Pure Function
  2. functional programming
  3. 函數副作用
  4. Functor and monad examples in plain Java
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容