上一篇的解答:
fun <D, A> List<Reader<D, A>>.liftReader(): Reader<D, List<A>> {
return Reader { d ->
this.map { innerReader -> innerReader.run(d) }
}
}
本章節來做點小回顧與整理
在 Category theory 那一篇中,指出了我們的人腦是怎麼解決問題的,從大問題拆解為小問題,最後一直拆到無法被拆解為止。仔細想想,這個模式其實到處是啊!舉例來說,在敏捷開發(Scrum)當中,其中一種常用的估時工具為 Story point,然後這些分數的組成,只能是費布納西數列 - 例如 1, 2, 3, 5, 8, 13。然而,用這數列估時有什麼好處呢?舉例來說,一旦有一項任務被估到了 13 分,就表示這是一個困難的任務,要花相對多的時間來完成,而且這數字也代表了不確定性很高,所以為了降低不確定性,通常我們都會避免 13 分的出現,希望能夠拆成2-3個任務或是更多。
忘記在哪一本書中看到,把分數非常大的任務比喻成做火箭,如果現在隨便抓一個工程師,問他如果要做一個火箭要做多久?他應該回答不太出來,還有可能會出現一個時間區間非常大的答案,但是如果現在問的問題是,能在太空上飛的玻璃要花多久時間才能完成,他就可以給你一個精準的答案。
這段一項任務拆成多項任務的過程不就是在分解問題嗎?而這些被拆分的任務完成時,會將多項任務的產出組合起來變成一個,這就是 Composition 不是嗎!而在這 functional programming in Kotlin 系列中,從前面的 function composition 的 compose
, pipe
,到中間 Functor 的 map
,Monad 的 flatMap
,還有第十四篇 Lenses 的 ComposedLenses 。 到處都充滿了 Composition 的蹤跡。
所以 functional programming 的 composition 這概念,非常適合用來解決我們碰到大大小小的問題,再搭配上好的抽象,好的抽象加好的組合,就能寫出清楚、表達力良好、好維護的程式碼。
在物件導向中,抽象化也是個重要的概念。由於我們大腦的空間是有限的,相信任何人都不想看到一萬行以上的程式碼,一堆共享變數充斥在每個角落,還有永遠也檢查不完的 null check。所以我們需要 function,需要 class,需要 module 來好好的將職責分離出來。有了這些職責分離的方法後,才能好好的專心在某一個面向來解決問題,而不用被各種實作細節所干擾。像是 MVC、MVP、MVVM ,就可以讓我們專心把 UI 的實作細節放在 View ,資料的儲存、快取、抓取策略都放在 Model ,剩下的核心邏輯就更好處理了,同時也更好測試了。
DRY - Don't Repeat yourself ,在 functional programming 的世界中也是非常適用的
物件導向的抽象化相對好理解,大多數都是“有形的”、“類比的”或是我們都非常熟悉的單字,像是上面所說的 MVC 的 View(視圖) 、 Factory patterm (工廠) 或是 Template pattern (樣板)。然而 Functional Programming 的抽象化卻是真的非常抽象,沒有任何現實生活中的概念可以拿來比喻,所用的單字也像火星文一樣讓人難以消化(Monoid, Functor 等等)。
然而就像之前章節說的,這些抽象化都是建立在對數學的理解上。像是我想要有一個結對的資料結構,但是同時間只會有一其中一個存在,雖然這種模式很常出現,但我們在寫程式時卻不會很容易的意識到。舉例來說,網路連線不是成功就是失敗、寫入檔案也有可能成功或失敗、要求權限也有可能成功或失敗。這些都可以被一個資料結構 Either
給取代,而且想要對這資料進行操作時, map
, flatMap
, fold
, mapLeft
這些 operator 都可以共用,只需要實作一次就好,非常方便,但是如果是物件導向學過來的工程師,是不熟悉這樣的思考模式的。以下再舉一些例子:
這些都是 Monad ,也有些人稱這些是 Functional programming 的 Design pattern。
物件導向中有一個 SOLID principle,只要好好的掌握這些原則,就能寫出好維護、可測試性高、可讀性高的程式碼。雖然我不知道在 functional programming 是否有同樣地位的原則,但是的確是有一些原則要去遵循的。
在前面章節中介紹的 Pure function、immutability、No side effect ,在 functional programming 中是非常重要的,如果沒有好好的遵循這些原則的話,Functor 的其中一項特性:結合律,將不復存在,因此使用 map
、 flatMap
這些 operator 也將無法保證程式的正確性,導致產生的 bug 更加難以追蹤。
// mutable structure
class Point(var x: Int, var y: Int)
// mutable reference
var a = Point(3, 4)
var b = Point(5, 6)
// global refernece
val originalPoint = Point(2/width, 2/height)
// 處處充滿危機的程式碼,可變的變數 + 全域變數,在這樣的狀態中,不會有結合律
val someList = listOf(a, b)
.map { it + originalPoint }
對於我來說,學習 Category theory 的最大收穫是,他讓我了解到這些 Functor 、 Monoid 、 Monad 的背後是有數學理論證明他的正確性的,有了這些知識之後,讓我知道何時應該使用它,怎樣的情況下使用會不安全。
坦白說,我也沒有完全懂 Category theory 或是 Functional Programming 的全貌,但是既然這些理論的最後目標是 - “確保程式的正確性”,就要想想怎樣才能發揮他的最大價值。在我們日常開發中,時間永遠不夠用,有著接不完的 feature,有著解不完的 bug 。有了 “確保程式的正確性”的這個指引之後,就可以把更多時間精力放在最不能出錯、邏輯最複雜的地方,並且使用“安全”(functional)的寫法完成程式碼。然後,將比較少的精力放在非常明顯就能看到結果的地方(例如有沒有真的呼叫 Api,其實只要實際跑過一次就能驗證了),如此一來,bug 也越來越少了,PM 開心,老闆開心,你也會跟著加薪!