應該很多人都是從 Java 轉到 Kotlin 陣營的吧?不管是面試或是隨意聊天,在我問別人喜歡 Kotlin 的什麼地方的時候,最常聽到的答案就是 null safety 了。那 Null safety 真的這麼好嗎?在我們看了這麼多"容器"跟 Algebraic Data Type 之後,我們可以怎麼重新來看待 null safety 這件事?他真的帶來了我們什麼好處呢?
在 Java 中,沒有 immutableList 的概念,最頂層的 java.util.List
介面中就有 add
與 remove
這兩個 function ,因此我們無法預防任何人去修改這個 List 。但是總還是有這個需求啊!不希望任何人去修改我所指定的 List ,不然我無法放心的把這個 List 再丟給其他類別,萬一,如果真的有笨蛋修改了這個 List 的內容,產品上線後發現有 Bug 了,由於介面都是 List
,也不一定能夠追到產生這個 bug 的兇手是哪一個類別,所以該怎麼辦呢?好險, Java 的 Collections
有提供一個 API 能夠確保我們不會去修改到這個 List ,請看下面這段程式碼:
public class CollectionsDemo {
public static void main(String[] args) {
// create array list
List<Character> list = new ArrayList<Character>();
// populate the list
list.add('X');
list.add('Y');
System.out.println("Initial list: "+ list);
// make the list unmodifiable
List<Character> immutablelist = Collections.unmodifiableList(list);
// try to modify the list
immutablelist.add('Z');
}
}
當經過 unmodifiableList
的處理之後,如果嘗試著修改它,就會丟出一個 Exception 。如此一來,就可以防止其他人修改到了。但是,這有一個很大的缺點,就是要真正的執行到這段程式碼才會發現,原來這個 List 不能被修改,通常發現的時候已經浪費了很多時間了。
相對的, Kotlin 的 List
完全沒有 add
或是 remove
可以用,所以根本也不會有機會修改到,就算硬寫了,也會有 compile error。一個是Java 的 Runtime exception ,另一個是 Kotlin 的 Compile error ,發現錯誤的時間是有差距的。發現錯誤的時間越早,就有越多的時間做更多的事,更加有產出。
以這個概念再做延伸,我們發現 Java 的 null
也是一樣的道理,當我們在寫程式時,其實大部分的程式碼,是預期不會有 null 的可能性的。但是因為 Java 預設每個 Object 都可以是 null 的,就逼得我們這些工程師不得不接受這個悲慘的事實,甚至還為了預防 NullPointerException ,在每個地方都加上 null check ,每個地方都要加上錯誤處理:
class Point {
...
}
int getDistance(Point point) {
if (point == null) {
// -1 是什麼意思? 給下一個工程師玩猜猜樂?
return -1;
}
// Do something else
....
}
事實上,在很多時候 null 是不會發生的,這種浪費時間又干擾閱讀程式碼的雜訊一直在摧殘著我們新鮮的肝與腦,看看 Kotlin 的版本多棒啊!
class Point(val x: Int, val y: Int)
fun getDistance(point: Point): Int {
// 不用檢查 null ,太棒啦~~
// Do something
...
}
如果用 Algebraic Data Type 的方式來思考的話,Java 的每個物件型別都是 Sum Type ,一個跟 Null 組合起來的 Sum Type 。想想看,在正常情況下,我們不會每個型別都另外加上 Maybe 吧?這樣多奇怪啊?但是 Java 正是一直在做這樣的事情。
// 有必要嗎?可能會有空值的情況嗎?
val point = Maybe.just(Point(1, 2))
另外之前有學過, Maybe 所帶來的資訊量,是原本所帶的資訊量再加 1,如果是一個 Maybe ,其資訊量是 (2 + 1 = 3),而這個多出來的 1 (nullable)在 Java 通常是一種浪費,一個資訊浪費,所以在有了 null safety 之後, Kotlin 因為沒有這樣的資訊浪費,可以寫出比 Java 更有表達力,更少 Bug 的程式碼,在一定的時間內有更多產出(當然前提是要會正確的使用 Kotlin 的這特性)。
相信很多人都很重視測試,而且還很熟悉他們。測試金字塔中的單元測試、整合測試、端點對端點測試(End to End testing),都可以在一定程度上保證專案程式碼的正確性。其中單元測試能夠最早發現問題,End to End testing 最晚發現問題,所以單元測試很重要,如果寫的好的話可以在非常早的階段中發現錯誤,節省很多人力成本。反過來說,最沒有效率情況是,如果產品已經上線了,被使用者發現 bug ,我們要花費的人力資源可能就橫跨了 QA、PM、Engineer、甚至有可能是老闆,花費的資源可能最少是以“天”為單位,這絕對是大家都不願意看到的。
那麼最好的情況是什麼?在什麼時候發現問題最快?是單元測試嗎?其實不是,是設計出一個能夠完美符合需求,具有強烈表達能力的 Type ,將上面所說的“資訊浪費”量降到最低,就可以在 Compile time 發現問題,例如上面的兩個例子:java 的 immutable List 跟 nullable object 與相對應的 Kotlin 帶來的解法,都是因為有了更強烈、更合適的介面來降低了我們犯錯的機率。而這花了多少時間發現問題?通常是以“秒”為單位。
Algebraic Data Type 提供了我們對 Type 的另一種解讀方式,可以使用數學這套工具來知道,現在這個 Type 到底有多少種可能的值,其中這些資訊,又有哪些是不必要的、可以省略的。至於資訊是怎樣浪費的,忘記的朋友可以回想一下之前的範例(乘法的數字遠比加法大):
// Product Type,太多無意義的可能性
data class ResultA(val data: String?, val fail: Throwable?) {
fun isSuccess(): Boolean {
return fail == null && data != null
}
}
// Sum Type,完美符合所有的可能性
sealed class ResultB() {
class Success(val data: String): Result()
class Fail(val error: Throwable): Result()
}
以上算是我對於學習 functional programming 中所體會到的一點小心得,另外,這個概念對於物件導向也是一樣的,不是只有 functional programming 才可以運用這概念。希望這篇對各位讀者有所幫助。
[Kotlin conf 2019] The power of Types: https://www.youtube.com/watch?v=t3DBzaeid74&t=12s