上次的解答, fold
在之後還會看到它的,在 functional programming 中是一個常見的 operator:
fun fold(success: (T) -> Unit, fail: (Throwable) -> Unit) {
when(this) {
is Success -> success(this.value)
is Fail -> fail(this.error)
}
}
在看了 List, Map, Try 之後,是不是開始抓到了一些感覺了呢?感覺起來...他們好像都有點共通性,都有 map
, filter
, fold
這些 operator。另外,在之前的章節說過,這些 operator 是一個用來做 functional composition 的強大武器。說到這,應該就有不少人想到,有另一個很熟悉的工具,也有著許許多多不一樣的 opeartor ,他就是擁有著高學習曲線,在 Android 中非常著名的程式庫 - RxJava。
想當初我剛學 RxJava 時,是只有物件導向基礎的,那時候學起來非常痛苦,完全無法理解為什麼這樣設計,Observer、Subscriber、Observable,到底什麼是什麼?又有什麼差別?直到兩年後有接觸到 Reactive programming 跟 functional programming 才漸漸的知道該怎麼使用他,以及他們的原則與範式(paradiam),最後,才真正的覺得自己有在寫 Reactive functional programming。
由於篇幅的關係,本系列文章將不會介紹 Reactive programming 的部分,只會針對 functional programming 來講解。但我相信只要了解了 functional programming 對 RxJava 的意義的話,就能更加了解 RxJava 的設計,以及背後想解決的問題,在使用起來會更加得心應手!
說到 RxJava 的核心類別,一定非 Observable
莫屬。那 Observable 的基本用法是什麼呢?
val oneElementObservable = Observable.just(1)
val multiElementObservable = Observable.fromArray(1, 2, 3)
其實跟 List 非常像,對吧?只要把 Observable 的 factory method 轉成 listOf()
的樣式,不就是幾乎一模一樣了嗎?
val oneElementObservable = observableOf(1)
val multiElementObservable = observableOf(1, 2, 3)
val oneElementList = listOf(1)
val multiElementList = listOf(1, 2, 3)
fun <T> observableOf(value: T): Observable<T> {
return Observable.just(value)
}
fun <T> observableOf(vararg values: T): Observable<T> {
return Observable.fromArray(*values)
}
而這對 functional programming 來說有什麼意義?我們可以想像成 Observable 跟 List 其實都是一個“容器”,只是容器的運作方式不同罷了,更重要的是,“容器”跟“容器”之間是可以搬移內容物的,下面以程式碼說明,這邊先稍微帶過即可,之後的文章中會再詳細說明。
val observable: Observable<Int> = observableOf(1, 2, 3)
val observableToList: List<Int> = observable.blockingIterable().toList()
val list: List<Int> = listOf(1, 2, 3)
val listToObservable: Observable<Int> = Observable.fromIterable(list)
接下來說到 Observable 另一個重要的元素:Error。
val normalObservable: Observable<Int> = Observable.just(3)
val errorObservable: Observable<Int> = Observable.error(RuntimeException())
等等...這有點眼熟對吧?上一篇才看過類似的程式碼不是嗎?
fun average(sum: Int, count: Int): Try<Int> {
return try {
Try.Success(sum / count)
} catch (e: Throwable) {
// divide by zero
Try.Fail(e)
}
}
// Try.Success 對應到 Observable.just
// Try.Fail 對應到 Observable.error
看來 Observable 也做到了 Try 要做的事情,不是嗎?那麼, Try 也是容器嗎?看起來不太像,對吧?但是換個角度想想,Try 同時包含了兩種可能的狀態,不是“成功”就是“失敗”,不就也算是一種容器嗎?而且,還有一個非常雷同之處:
val observable: Observable<Int> = Observable.just(3)
observable.subscribe( {number: Int ->
println(number)
}, {error: Throwable ->
println(error.message)
})
val tr: Try<Int> = Try.Success(1)
tr.fold( {number: Int ->
println(number)
}, {error: Throwable ->
println(error.message)
})
Observable 的 subscribe
跟 Try 的 fold
竟然這麼相似!
Observable 跟 Try 還有另外一個非常重要的特性:只要發生錯誤,接下來的 operator 都沒有任何作用!( 當然,onError 系列除外)看看下面的例子:
class AccountRepo() {
// 從 id 找不到 Account 的話會丟 Exception
fun queryFromId(id: String): Account {...}
// account 必須還在,有可能被其他人不小心刪掉,如果沒有相對應的 account 一樣會丟 Exception
fun updateAccount(account: Account): Boolean {...}
}
class BankService() {
// 帳戶的錢必需還要夠多,不夠的話會丟 Exception
fun withDraw(account: Account, amount: Int): Account {...}
}
fun withDrawMoney(accountId: String, amount: Int) {
Observable.just(accountId)
.map { id: String -> accountRepo.queryFromId(id) }
.map { account: Account -> bankService.withDraw(account, amount) }
.map { account: Account -> accountRepo.updateAccount(account) }
.subscribe(...)
}
在 withDrawMoney 這個 function 中,有主要三個步驟:尋找帳戶、提款、更新帳戶。其中的任何一個步驟都有可能因為發生一些“意外”而無法跑完整個流程(注意到我用“意外”這個詞了嗎),有可能是找不到用戶,也有可能是錢不夠。當今天一個“意外”發生了,而且發生在第一個步驟,這時會丟出一個 Exception 。然後第二步驟跟第三步驟呢?既然連帳號都找不到了,就不需要被執行了吧!事實上 Observable 就是如此運行的,只要有任何一個地方發生錯誤的話,之後的 operator 將會直接忽略過,然後交給 subscribe
來處理。(當然 onError 系列除外: onErrorReturn
, onErrorResumeNext
, retry
等等)。而這樣的錯誤處理有什麼好處呢?
onErrorReturn
可以更加容易的各別處理錯誤。return
, try-catch-finally
, ?.let{}
, :?
等等下面示範了其中一種可行的做法,如果找不到帳號,就幫他建立一個新的。
Observable.just(accountId)
// 將每一個步驟拆解成函式,就可以依據需求更自由的組合、操作
.map { id: String -> accountRepo.queryFromId(id) }
// onErrorReturn 可以直接處理現在碰到的 Error,回傳新的狀態,然後繼續進行下一個步驟
.onErrorReturn { Account(...) }
.map { account: Account -> bankService.withDraw(account, amount) }
.map { account: Account -> accountRepo.updateAccount(account) }
各位還記得 side effect 嗎?Exception 在上一個章節被當成了一個 Side effect,所以用 Try 這個“容器”來將他包裝起來 。在這裡,Observable 也用了類似的概念,更棒的是,RxJava 提供了各式各樣的 operator 來讓我們來搭配使用:像是 map, filter, reduce, onErrorReturn 等等,不用辛苦的自己手動實作。
Observable 是一個“容器”,其“容器”的內容有可能有零到多個元素,也有可能包含一個 Error。“容器”這個概念呢,在 functional programming 非常常見,之後還會介紹更多。同時,在 functional programming 中可以善用這些 function 小區塊,利用各種不同順序的組合函式,來更妥善的處理各種 Error ,讓我們更專注在流程而不是細節。然而, Observable 還有一項主打:非同步。這時問題就來了:那非同步也是“容器”的一種樣貌嗎?以往我們所熟悉的 Future、Promise 又可以怎樣連結到 Observable 呢?這裡留給讀者一點想像空間,別急,functional programming 的世界才剛始而已呢!