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

W_BinaryTree發表於2017-12-11

前言: 上篇文章介紹了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本身。 用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和 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)沒有任何區別
複製程式碼
  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的意義不必太過糾結,只需將其理解為一種對集合進行組裝變換的一種解決方案即可。

參考文獻(部分連結可能需要梯子)

  1. Pure Function
  2. functional programming
  3. 函式副作用
  4. Functor and monad examples in plain Java

相關文章