iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 6
0
Software Development

Functional Programming in Kotlin系列 第 6

Non-deterministic, side effect and Try

今天要來談談“意外”這件事,沒有人喜歡“意外”對吧?尤其是 PM 或是 QA 的神之手,有時候就是會給你“意外”的操作出一些 bug ,而你又重現不出來時,要怎麼辦?難道只能兩手一攤說:在我這邊的環境都沒問題啊!

那如果我們工程師都不喜歡“意外”了,數學家當然也不意外。於是 functional programming 就非常重視這一塊,甚至還可以做到完全避免“意外”的發生。

Non-deterministic

deterministic 這個字的意思是“確定性的”,前面加一個 non ,當然就是反過來的意思“不確定性”。現在來一起想看看,在日常的程式開發中,有哪些東西是不確定的呢?其中一個很簡單的答案是:亂數。你可能想說,當然啊!我就是要隨機產生數字才使用亂數的,要是非常“規律”、非常“確定性”,那麼這數字也亂不起來了對吧。那這邊有一個有趣的問題,剛剛說 functional programming 不喜歡意外,我們就完全不使用 Random 了,對嗎?

還有什麼是不確定的?下面再舉幾個例子:

  1. 網路連線狀態:2G, 3G, wifi 或是沒網路都有可能發生。
  2. 檔案讀寫:空間不足、壞軌
  3. 任何其他硬體裝置:相機、GPS、藍芽...
  4. Third party library:這裡有很多埋好的地雷等你來踩...

Side Effect

從剛剛的主題的延伸,我們就會再講到下一個概念 - 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)

Try

講了這麼多 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 喔!


上一篇
Partial function and total function
下一篇
[RxJava] Observable and error handling
系列文
Functional Programming in Kotlin30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言