今天要來談談“意外”這件事,沒有人喜歡“意外”對吧?尤其是 PM 或是 QA 的神之手,有時候就是會給你“意外”的操作出一些 bug ,而你又重現不出來時,要怎麼辦?難道只能兩手一攤說:在我這邊的環境都沒問題啊!
那如果我們工程師都不喜歡“意外”了,數學家當然也不意外。於是 functional programming 就非常重視這一塊,甚至還可以做到完全避免“意外”的發生。
deterministic 這個字的意思是“確定性的”,前面加一個 non ,當然就是反過來的意思“不確定性”。現在來一起想看看,在日常的程式開發中,有哪些東西是不確定的呢?其中一個很簡單的答案是:亂數。你可能想說,當然啊!我就是要隨機產生數字才使用亂數的,要是非常“規律”、非常“確定性”,那麼這數字也亂不起來了對吧。那這邊有一個有趣的問題,剛剛說 functional programming 不喜歡意外,我們就完全不使用 Random 了,對嗎?
還有什麼是不確定的?下面再舉幾個例子:
從剛剛的主題的延伸,我們就會再講到下一個概念 - Side Effect 。在之後的章節會越來越常看到這個字,事實上,不管是 code review 、架構設計的討論上、或是日常的需求開發,都需要經常談論到他。Side Effect 在字面上的翻譯是“副作用”,那麼,什麼東西算是副作用呢?
剛剛所說的 non-deterministic 就是其中一種副作用,下面來舉個例子:
fun saveImageAsFile(bitmap: Bitmap): File {
// copy file implementation
....
}
輸入是一個 Bitmap
(一種在 Android 的圖像格式),輸出是一個 File
。乍看之下沒什麼問題,是一個 Pure function 。但是就如同剛剛所說的,如果在產生檔案的過程中,記憶體不足的話會發生什麼事?會丟出一個 Exception !那這件事我們有辦法控制嗎?就算提前知道記憶體空間有多少了,做了一些機制來防止這個錯誤發生,但是你有辦法防止所有的 IOException 嗎?
再來舉一個常見的例子 - Date & Time。不正確的日期操作方式,也會讓 function 有 side effect:
fun createNewAccount(val name: String): Account {
return Account(name = name, createdBy = Calendar.getInstance().timeInMillis)
}
有看出上面這段程式碼的問題了嗎?他使用了 Singleton!這種無法控制的外部因素,也會讓一個 function 有 side effect。
讓我們做個簡單的定義,這些隱性(implicit)的錯誤、外部相依、或是“意外”,不管從 function 的輸入、輸出或是函式名稱都不是很明顯就能知道的。這些不是預期中會發生的作用,就稱作 side effect。
因此,有 Side effect 的 function ,同時也不會一個 pure function。他們是一個互斥的關係: Side effect → Non-pure function , Pure function → no Side effect。
要怎麼讓時間日期沒有 side effect?使用 Dependency Injection 就可以秒解!像是上面的範例就可以改成:
createNewAccount(val name: String, val time: Long)
講了這麼多 side effect 所造成的問題,總得要提出解法吧?Try class 就是其中一項對付 side effect 的解法:
sealed class Try<T>{
class Success<T>(val value: T): Try<T>()
class Fail<T>(val error: Throwable): Try<T>()
}
這是一個非常簡單的類別,只有兩種 case :不是成功就是失敗,那要怎麼使用他呢?
// 註:這只是範例,正常求平均值不會這樣寫
fun average(sum: Int, count: Int): Try<Int> {
return try {
Try.Success(sum / count)
} catch (e: Throwable) {
// divide by zero
Try.Fail(e)
}
}
Side effect 被 Fail 給包裝起來了,如此一來,每一組輸入都有一樣的輸出,這個 function 開始變“pure”了。那下一個問題是,如果要得到計算過後的值要怎麼辦呢?
fun foo() {
val sum = 15
val count = 3
val average = average(sum, count)
when(average) {
is Try.Success -> print(average.value)
is Try.Fail -> print(average.error)
}
}
使用 when
來判斷型別就是一個最簡單的方式。 等等,這樣好像變得更麻煩了吧!我一開始用 try catch 包起來不就好了嗎?這樣子使用 Try 包來包去多麻煩啊!但是別忘了,functional programming 的最大特徵是什麼? Composition!正常來說不可能只有一個 function 這麼簡單,而是有許許多多的 function 要被組合起來!
fun foo() {
val sum = 15
val count = 3
val average: Try<Int> = average(sum, count)
// 想要對 average 的結果再做一些計算
val result = average.map{ it + 2 }
.map { "number: $it" }
when(result) {
is Try.Success -> print(result.value)
is Try.Fail -> print(result.error)
}
}
sealed class Try<T>{
class Success<T>(val value: T): Try<T>()
class Fail<T>(val error: Throwable): Try<T>()
// map 在這裡又看到他了,一樣是一對一的概念,只是這裡變成了 Try<T> -> Try<R> 的對應關係
fun <R> map(transform: (T) -> R): Try<R> {
return when(this) {
is Success<T> -> Success(transform(this.value))
is Fail -> Fail(this.error)
}
}
}
如果上面的 average
出了問題了,後面的兩個 map
也不會對結果有任何影響,一樣還是會回傳一開始發生的 Fail
。所以在你需要延遲處理例外狀況時, Try 是一個很好用的類別,可以把處理例外狀況放到最後,在中間完全不需要理會出了什麼狀況,利用 map
來做一些我想要的計算,同時可以放心的不需要去理會錯誤狀態。
在處理 Try 的結果時,會覺得每次都要寫 when 很煩嗎?其實有一個常見的 operator 叫做 fold
,他是這樣使用的:
val sum = 15
val count = 3
val average: Try<Int> = average(sum, count)
average.map{ it + 2 }
.map { "number: $it" }
.fold(success = { value: String ->
print(value)
}, fail = {error: Throwable ->
print(error.message)
})
是不是簡單多了呢?相信你也猜到任務了,今天的任務就是要實作出 fold
這個 operator!提示:輸入是兩個 Function type 喔!