iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 5
1
Software Development

Functional Programming in Kotlin系列 第 5

Partial function and total function

上一篇的解答:

private fun <K, V, R> Map<K, V>.fmap(transform: (V) -> R): Map<K, R> {
    val destination = hashMapOf<K, R>()
    entries.forEach { (key: K, value: V) ->
        destination[key] = transform(value)
    }
    return destination
}

不知道讀者你比較喜歡原本的 map ,還是這個版本的 fmap 呢?


在正式介紹 partial function 之前,我們來畫幾張 Mapping diagram 來幫助了解這概念:

One to One mapping

集合中的每個元素剛好都有相對應到另外一個集合的元素

https://user-images.githubusercontent.com/7949400/93023757-b9d12080-f623-11ea-8dba-0db03dadc178.jpeg

Many to One Mapping

在集合中,一個以上的元素對應到另一個集合的同一個元素

https://user-images.githubusercontent.com/7949400/93023756-b50c6c80-f623-11ea-8d53-1fee54211346.jpeg

在寫程式時,最常見的就是這兩種對應關係了。然而,有的時候左邊的集合是沒辦法全部對應到右邊集合的元素,例如下面這個函式:

https://user-images.githubusercontent.com/7949400/93023754-ade55e80-f623-11ea-8ed6-a6453ddc2319.jpeg

上面是一個 String 轉 Int 的函式,當輸入是 Hello 時,是沒有相對應的數字的,有著這樣的特性的函式,我們會稱為 Partial function。 Kotlin 在遇到 Partial function 時,有可能會因為接收到不預期的輸入,而丟出一個 Exception 。通常我們會用一些防呆機制或是 try catch 來處理這樣的狀況。

然而在 functional programming 中,我們不喜歡 Partial function,最好是所有的 function 都是 Total function。Total function 是什麼呢?就是每一個 input 一定可以對應到 output 的其中一個值,如此一來我們就不會有任何例外發生,像一開始舉的兩個例子:f(x) = x + 1 與 f(x) = x * x 就是 Total function。

Code sample

val toIntLambda = { str: String -> str.toInt() }
val toMonthLambda = { number: Int -> Month.of(number) }

val result1 = listOf("1", "2", "3")
    .map(toIntLambda)
    .map(toMonthLambda)
// [JANUARY, FEBRUARY, MARCH]

val result2 = listOf("11", "12", "13")
    .map(toIntLambda)
    .map(toMonthLambda)
// java.time.DateTimeException: Invalid value for MonthOfYear: 13

val result3 = listOf("a", "b", "c")
    .map(toIntLambda)
    .map(toMonthLambda)
// java.lang.NumberFormatException: For input string: "a"

由於上面兩個函式 toIntLambdatoMonthLambda 都是 Partial function ,所以當輸入是無法轉成輸出時,會丟出 Exception 導致無法執行下一段程式,那我們可以怎麼修改呢?

val toIntLambda = { str: String ->
    try {
        str.toInt()
    } catch (e: Exception) {
        -1
    }
}
val toMonthLambda = { number: Int ->
    try {
        Month.of(number)
    } catch (e: Exception) {
        null
    }
}

val result1 = listOf("1", "2", "3")
    .map(toIntLambda)
    .map(toMonthLambda)
println(result1)
// [JANUARY, FEBRUARY, MARCH]

val result2 = listOf("11", "12", "13")
    .map(toIntLambda)
    .map(toMonthLambda)
println(result2)
// [NOVEMBER, DECEMBER, null]

val result3 = listOf("a", "b", "c")
    .map(toIntLambda)
    .map(toMonthLambda)
println(result3)
// [null, null, null]

現在這兩個 lambda 都用 try catch 包起來了,變成了 Total function 。這樣一來,至少每一段程式碼都能成功執行。但是,這真的是一個好的解法嗎?

首先第一個函式 toIntLambda,以 -1 來代表無法被轉換成功的 String ,但是萬一輸入就剛好是 "-1" 呢?一開始我就是想拿 -1 來計算不行嗎,但是現在 -1 有兩個不同的含義了,會讓後續在追蹤問題時更難維護。至於第二個函式 toMonthLambda 使用了 null 來代表錯誤的值,這也有其他問題,萬一之後有第三、第四個 Partial function 採用一樣的做法,我們有辦法知道是在那一個步驟發生問題嗎?以我們常見的登入系統為例,常見的錯誤有:email 格式錯誤、使用者沒有連上網路、使用者不存在、伺服器無回應。如果這些都用 null 來代表錯誤狀態,使用者將無法得知她需要採取怎樣的動作來回應這錯誤。

所以有更好的解法嗎?當然是有的,下一篇將會介紹如何使用 Either 來做錯誤處理。


練習時間

今天沒有實作練習,但是還是給各位讀者一點作業:在你最近寫的程式當中,有哪些是 Partial function?又有哪些是 Total function?至少各找出五個。在你找到的 Partial function 之中,有好好的處理例外的情況嗎?你最常怎樣來處理例外呢?


上一篇
Kotlin collections: List, Map and Set
下一篇
Non-deterministic, side effect and Try
系列文
Functional Programming in Kotlin30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言