理解函數語言程式設計語言中的組合--前言(一)
函數語言程式設計思想可以用一句話總結,即:可組合的型別+可組合的函式,我在《使用函式式語言做領域建模》一文描述瞭如何使用可組合的型別進行領域建模。這篇文章就是用來說明後半部分,即--理解可組合的函式。我假設讀者已經對“Higher order function”, “Currying“, ”Immutable“等基本概念有所瞭解,並擁有基礎的TypeScript知識即可。
這一切還得從抽象說起。
抽象的重要性
人類能夠解決複雜事物的一個重要方法就是抽象,程式碼也一樣。有了抽象,你的程式碼才不會變成流水賬,實際上那些讓人讀起來賞心悅目的好程式碼,一定擁有良好的抽象。抽象可以讓你避免陷入細節,通過幾個重要的類名或者介面,讓別人快速識別你的設計。
當然,這一切的前提是,閱讀程式碼的人也擁有類似的抽象思想,他才能夠心領神會你的意圖。如果你在讀程式碼的時候,自己是怎麼想的,正好程式碼就是這樣設計的,你才會覺得程式碼可讀性太強了。
怎麼樣大家才能設計出一致的抽象呢?
《設計模式》就是這樣一本把常見問題總結成一些列模式的書籍。換句話說,《設計模式》提供了常見問題的抽象方式並提供了相關的術語。
當然你的設計好不好,還取決於你是否正確識別到了問題的本質,並恰如其分的實現了某個已知的設計模式。如果閱讀程式碼的人擁有跟你類似的抽象知識,他就能夠迅速明白你的設計和意圖。
函數語言程式設計中的抽象
如果說《設計模式》總結了OO思想中的常見抽象方式,那麼函數語言程式設計語言也擁有自己的抽象方式。
看下面的這兩行數學運算:
1 + 2 = 3
這行數學運算非常簡單,以至於靠直覺就能判斷他的真確性。這行程式碼可以描述為:兩個“數字”通過“相加”任然為“數字”。
如果我們把"數字“泛化,將"相加”推廣開來,就可以用在其他事物身上,例如:
"a" + "b" = "ab"
對字串也是適用的,兩個字串通過一個運算子合併為一個新的字串。沒錯,這就是組合的基礎。
函數語言程式設計中的抽象叫做《範疇輪》Category theory,是一門研究如何組合事物的數學科學。其中,“事物”被稱為object, 但這個object並不是OO中的那個物件,而事物之間的轉化或者對映被稱為morphisms,翻譯為中文叫射態,對應到程式語言中,就是函式。《範疇論》在函數語言程式設計語言中的地位,可以類比《設計模式》在OO中的地位。
為什麼需要《範疇論》
這要從函式的組合開始說起,我們知道函數語言程式設計語言沒有OO語言中的那些概念,例如類,繼承,依賴注入(有的語言有FP和OO兩種正規化, 例如Scala, F#等,請不要糾結)。只靠函式,為大型的工程化實踐帶來了挑戰。 玩過樂高的同學都知道,樂高的零件都是擁有獨立功能的小部件,然而,卻有人用它拼出了汽車,飛機甚至是航空母艦,這充分說明了組合的力量。
那麼函式是如何組合的呢?看下面的程式碼:
const add = (a: number, b: number) => a + b
const sub1 = (a: number) => a - 1
宣告兩個函式add和sub1, 像lodash或fp-ts等庫都會提供一個叫flow的函式,flow的作用就是把若干個函式組合起來:
const addThenSub1 = flow(add, sub1)
expect(addThenSub1(1, 2)).toBe(2)
此時addThenSub1變成了一個新的函式,最後再將引數作用在上面就可以得到結果。flow理論上可以支援任何數量的函式組合。然而,flow有個致命的問題,他要求上一個函式的返回值型別跟下一個函式的輸入型別一致,例如,上面例子中的add如果返回string, 那麼連線就失敗了。另外對sub1的引數數量也有要求,即除了第一個函式add, 後面的函式只能接受一個引數。
其實這兩條限制就為函式的組合判了死刑,即:
雖然函式是可以組合的,但並不是所有的函式都能任意組合。
《範疇論》就是這樣一門數學科學,他幫你抽象出了事物的轉化或組合模式,你只要按照他總結的模式設計,就能將函式組合起來。