iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 8
0
Software Development

Functional Programming in Kotlin系列 第 8

[RxJava] side effect operators and advanced operator

Side Effect Operator

前一篇介紹了 Observable 跟 Try 是如何處理Exception 這個 side effect,那麼可能有一個疑問說,是不是所有的 side effect ,Observable 都幫我處理好了?然後就可以開心的說,我正在寫很安全的程式碼,不用擔心任何意外,一切都在控制範圍內?

很遺憾的是,使用Observable來寫程式並不代表一切都是安全的,也不代表你正在寫 functional programming。有一個我很常聽到的迷思,使用 List 的 function chaining,或是使用 RxJava、或是有用到 lambda,就是在寫 functional programming 了。但是換個角度想,這是我個人的心得,你覺得怎樣才算是有在寫 Object Oriented Programming 呢?可能是有用到繼承?可能是會用 Design Pattern?或是精通 SOLID principle?這個沒有標準答案,這表示這個知識是一個演進的過程,只要你不停止成長,隨時都會有新的體悟。 Functional programming 也是同理,使用 function chaining 或是 lambda 只是 functional programming 的其中一個面向罷了,還有很多很多的知識需要去學。

現在拉回來,使用 Observable 的時候, 怎樣才是不安全的、有 side effect?下面舉的這個例子,只要你對 RxJava 有相關開發經驗的,一定會遇到:

// 紀錄更新次數
var numberOfUpdate = 0
  
fun updateUser(user: User): Observable<Boolean> {
    return userRepository.update(user)
        .doOnNext { numberOfUpdate ++ }
}

假設現在有一個需求,要紀錄使用者總共更新了幾次,由於 userRepository.update 的回傳值是一個 Observable ,最簡單的作法就是在後面加上一個 doOnNext 來紀錄更新次數,而這個更新次數,就是一個 side effect。因為 numberOfUpdate 對於這個 Observable stream 而言,是一個外部相依,而且對於 updateUser() 而言,numberOfUpdate 的存在也會讓他不是 pure function。所以我們可以進一步的說,doOnNext 就是一個為了產生 side effect 用的 operator !是的,你沒看錯,事實上這樣的 operator 還真不少,隨時改變外部變數還有可能會因此產生 race condition,因為你也無法確定是哪一條 Thread 在改變狀態,又是哪一條 Thread 在讀取狀態。

那麼,是不是完全不能用這些 side effect operator 了呢?當然也不是。functional programming 的原則不應該成為實務開發的絆腳石,不應該是一個至高無上不可違背的準則。反過來,我們應該要了解這些原則背後想要傳達的理念,再消化、寫出更安全的程式碼。在上面這段程式碼中我們怕 race condition 的發生,那我們可以怎麼做?第一個可以採取的作法是確保一切都發生在同一個 Thread 。第二種是使用 Thread-safe 的資料結構,例如 AtomicInteger。第三種,將一切都包裝起來,一起回傳,以下示範第三種:

fun updateUser(user: User, numberOfUpdate: Int): Observable<Pair<Boolean, Int>> {
    return userRepository.update(user)
        .map { it to numberOfUpdate + 1 }
}

但是這樣實在是不太好理解,很難知道 Pair 到底是什麼意思,為了讓下一個讀程式碼的人更好理解,可以利用 Kotlin 的 typealias

typealias UpdateStatus = Pair<Boolean, Int>

fun updateUser(user: User, numberOfUpdate: Int): Observable<UpdateStatus> {
    return userRepository.update(user)
        .map { it to numberOfUpdate + 1 }
}

經過這樣子的修改,我們把紀錄次數的責任交給了使用這段 function 的類別,然而這樣有比較好嗎?我沒辦法給出一個肯定的答案,還要考慮整個架構、團隊成員的接收度、團隊的 code convension 等等。但可以肯定的是,你已經意識到 side effect 了,不管使用哪種解法,程式碼的安全性已經提高了。

Advanced operator

終於要開始難一點的部分了,functional programming 的大魔王 - flatMap 。先從一個實際的例子開始吧!

class GoodList(val ids: List<String>)

class Good(val id: String, val name: String, val imageUrl: String, val price: Int)

fun getGoodList(): Observable<GoodList> {
    ...
}

fun getGoodDetail(id: String): Observable<Good> {
    ...
}

fun fetchGoodsFromServer(): Observable<Good> {
    return getGoodList()
        ...?
}

假設現在遇到一個需求,要在一個頁面顯示所有商品內容。但是呢,為了要完成這功能,必須要發兩次以上的 request 給後端!這雖然是一個很爛的設計,我們還是得要把這功能完成。過了一段開發時間後,假設 getGoodListgetGoodDetail 這兩個 function 都實作完成了,剩下的問題是,要怎麼把他們合併在一起,完成我們的需求呢?由於這兩個都是非同步的函式,所以回傳的類型需要是 Observable (對於懂 RxJava 的人來說,也許用 Single 會比較適合,但為了統一教學起見,還是不要介紹太多新概念,專注在 Functional programming 上就好)。至於在fetchGoodsFromServer 這邊,目標是從後端拿到所有商品內容,在這裡可以利用 Observable 的特性,他會在一個生命週期裡發送零到多個資料,在使用端(subscribe)這邊,一旦接收到資料,就可以將資料加到畫面中,所以回傳的型別不需要是 Observable<List<Good>>

要組合兩個函式的話,之前有學過一個 operator - map ,但是使用 map 之後會發現,回傳的型別變成了 Observable<List<Observable>> ,跟預期輸出不一樣!

// 型別是 Observable<List<Observable<Good>>>,好像不太對?
fun fetchGoodsFromServer(): Observable<List<Observable<Good>>> {
    return getGoodList()
        .map { list ->        
            list.ids.map { id ->
                getGoodDetail(id)
            }
        }
}

看來這需求有點難啊...讓我們簡化一下問題吧,只拿第一個商品的話要怎麼做呢?

fun fetchSingleGoodFromServer(): Observable<Observable<Good>> {
    return getGoodList()
        .map { list ->        
            getGoodDetail(list.ids[0])
        }
}

回傳的型別變成了 Observable<Observable> ,離成功接近了一小步了,這邊有兩層的 Observable ,那我可以把兩層的 Observable 變成一層嗎?當然可以!如果把一個 Observable 類比成一個維度,那麼這個問題就是一個二維降成一維的問題,我們稱這個動作為 flatten 。事實上,List 就有一個 flatten 的 operator,而且, flatten 可以跟 map 結合起來,變成了 flatMap 。現在,讓我們直接使用這個結合起來的 operator 吧!

fun fetchSingleGoodFromServer(): Observable<Good> {
    return getGoodList()
        .flatMap { list ->        
            getGoodDetail(list.ids[0])
        }
}

看起來不錯!那接下來的問題就是要怎麼拿到所有的 good 的詳細資訊了,還記得上一篇所說的搬移“容器”嗎?只要好好運用這概念,這問題就迎刃而解了

fun getGoodList(): Observable<GoodList> {...}

fun getGoodDetail(id: String): Observable<Good> {...}

fun fetchGoodsFromServer(): Observable<Good> {
    return getGoodList()
        .flatMap { list ->
            // 從“容器” List 搬到“容器” Observable
            Observable.fromIterable(list.ids)
            // 從 Observable<String> 換到 Observable<Good>
                .flatMap (::getGoodDetail)
        }
}

功能完成了!但是有一件奇怪的事發生了!使用端把所有的商品組合起來的時候發現,有時候商品 A 會比商品 B 先到,有時候是商品 B 比商品 A 先到,PM 說這是 Bug ,這要怎麼辦呢?

fun loadGoods() {
    goods = listOf()
    fetchGoodsFromServer()
      .doOnComplete { println(goods) }
			.subscribe { goods.add(it) }
}

var goods: List<Good> = listOf()
loadGoods()
// [ Good(name = A), Good(name = B)]

// a few moments later...

loadGoods()
// [ Good(name = B), Good(name = A)]

原來, flatMap 是不保證順序的,只要這些 function 是非同步的在執行,他只會依照執行完成的時先後來做排列,假如發送 request 的順序是 A, B ,但是 B 完成的速度比 A 快得話,最後的結果將會是 B, A。所以要是不希望順序改變的話,得要使用另一個保證順序的 operator - concatMap

fun fetchGoodsFromServer(): Observable<Good> {
    return getGoodList()
        .flatMap { list ->
            Observable.fromIterable(list.ids)
                 // 保證順序 
                .concatMap (::getGoodDetail)
        }
}

其實還有另一個 operator - switchMap ,這個就留給讀者自己研究了。


結語

今天介紹了 side effect operator 跟 flatMap, concatMapflatMap 在 functional programming 中是一個非常常見的 operator ,任何有 flatMap 的“容器”我們也會稱作是 MonadflatMap 只要常用的話,其實不會覺得他有多難,甚至還覺得他很方便。至於 Monad ,才是真真正正的大魔王,不知道各位讀者是否跟我一樣看了無數的 Monad 教學文章,講著 Functor、Applicative、Monad。但整篇看完還是不知道他寫的是中文還是火星文呢?沒關係,接下來還會有很多篇文章討論到他,我會嘗試用各種角度解釋 Monad ,最後,希望讓大家一起快樂地說出那句著名的話: A Monad is just a Monoid in the Category of Endofunctors, what's the problem?


上一篇
[RxJava] Observable and error handling
下一篇
More FlatMap : List and Try
系列文
Functional Programming in Kotlin30

尚未有邦友留言

立即登入留言