抽象思維 — Functional Programming 提升開發效率的真正原因
之前的文章《用 Functional Programming 挑戰用 10 行寫完狀態機》中介紹了 Functional Programming (FP) 的原則與好處,但是並沒有具體的說明 FP 的正確使用方式。這篇文章會從程式設計的抽象思維模式下手,說明 FP 為什麼可以幫助我們更高效的 coding,以及使用 FP 時應該具備的概念。
試著回答一個簡單的面試題
首先,我們先來考慮一個很簡單的題目:
給一個數列 X,求 X 中所有元素的平方相加開根號。
這個題目實作起來不難,我們可以快速寫出如下的 python 答案:
這時候面試官將題目變化一下,變成:
給一個數列 X,求 X 中所有「自然數」的平方相加開根號。
回憶自然數的定義是「>0 的整數」,因此我們的答案可能改成這樣:
但是這個時候,面試官再次變換了題目:
給一個 0-based 的數列 X,求 X 中所有「位置不能被 3 或 7 整除的自然數」的平方相加開根號。
你可能會想:雖然題目更複雜了,但我只要多加幾個 if 判斷就好了。但是事不過三,我們需要進一步思考:這個題目還會變化幾次?如果出現更複雜的條件怎麼辦?我會不會改到什麼不該改的 code?為什麼要一直變更題目?
題目一直在變,這就是我們在軟體開發裡面常常碰到的問題——需求變更。壞消息是,需求變更永遠沒辦法根除,永遠都有意外。
那麼,該怎麼一勞永逸的解決這個題目?
抽象思維
從上面的面試題裡面,出題者的問題雖然無法預測,但是基本不會脫離幾個方向,例如限制計算的型別、限制計算的位置、限制計算的數值。如果我們能夠找到所有題目的背後的共同點,我們就可以根據這個規律來 coding,確保未來的所有題目都可以透過同一個 code 來完成。
找到題目背後的共同點——這就是一個「抽象化」的過程:
抽象化就是剔除所有具象概念的細節、發現背後的共同特質。解決這類問題的思維方法,就是抽象思維。
生活中最常見的抽象化之一就是「符號」。符號用最簡單的呈現出複雜的概念,例如上圖的男人、女人、傷殘人士、小孩,都可以用一個符號來表示。這個符號去除了「人」的大部分細節,並保留大部分「人」都具備的特質,而創造符號的過程就是「抽象化」。
抽象化是 programmer 一個極為重要的思維能力,因為我們的工作中無時無刻都存在著抽象化。對於資料庫工程師,你必須要設計什麼樣的欄位可以代表一筆訂單/一個客戶;對於 Server 端工程師,你要設計什麼樣的 API 可以完成多數 client 的需求;對於 AI 工程師,你必須把現實問題抽象為數學問題。對大多數的 programmer 而言,無法掌握抽象思維的能力,就無法完成可靠的程式。
如何抽象我們的面試題
對於前面的面試題,我們可以把面試題抽象成這樣:
給一個 0-based 的數列 X,給 N 個條件,求 X 中所有滿足所有條件元素的平方相加開根號。
抽象化是有層次的,我們也可以進一步把「平方相加開根號」也抽象出來,但為了簡單起見我們先到此為止。因為抽象到這個等級,我們已經出現了一個問題:我們如何用 code 來表示「條件」?
OOP 的解法
說到抽象,我們很容易想到 OOP 裡面的 abstract class。因此試著用 OOP 來寫,可能會變成這樣子:
其實上面的 code 已經十分抽象了,透過繼承 Condition 並 override 不同的 check 函數就可以滿足各種變化的題目。但上述的 OOP 解法還是有幾個問題,例如:
- Condition.check 其實是一個 static method,但是為了把 Condition 作為物件傳進去所以宣告成一個 non-static method。
- 如果要新增條件,我們必須繼承新的 Condition。當題目變多之後,會有越來越多的 Condition 需要維護。
因為 OOP 把所有概念都當作 Object 的特性,在處理例如 Condition 這種概念的時候會顯得不是很直觀。這時候 FP 就可以發揮出更好的抽象能力,其中要介紹 FP 裡面的一個語言特性——Lambda Expression。
Lambda Expression
Lambda Expression 在 C++/Java/Python 等各種主流語言都有支援,可以用來宣告「沒有函數名的函數」。用 Lambda Expression 宣告的函數有幾個優點:
- 可以像變數一樣 copy/assign,也可以像函數一樣 call。
- 適合用在臨時需要定義簡單函數的時候。
- 可以把底層的 code 抽象成未定義的方法,把複雜度留給呼叫方。
通常 Lambda Expression 寫法如下:
--- Python 範例:
sqr = lambda x: x**2
op = sqr
print(op(2)) # 4--- C++ 範例:
auto sqr = [](float x) { return x * x; };
auto op = sqr;
cout << op(2.0); // 4.0
上述的 sqr 是一個方法,其本身可以直接用 sqr() 來呼叫,也可以複製到變數 op 裡面傳遞。簡而言之,Lambda Expression 讓函數可以跟變數一樣臨時宣告與使用。
知道了 Lambda Expression 的用法,我們可以用更簡單的寫法完成抽象的面試題。
FP 的解法
可以看到 code 明顯比 OOP 的寫法短很多,而越短的 code 可能產生 bug 的地方就越少,品質也會越高。以下逐行說明一下:
- line 1:輸入的 cond 是 list,如果有新增的條件可以加在裡面。
- line 2:zip 把 x_list 轉換成了 list of tuple,tuple 內容是 (value, index)。
- line 3-4:對 conds 裡面的每個函數 c,filter 函數會保留通過的 (x, i)。
- line 5:取出保留的所有 x,然後平方相加開根號。
這裡面用到了幾處 FP 的特性,例如 conds 在 line 1 與 line 3 像變數一樣被傳遞、被遍歷。在 line 4 的時候函數 c 也像變數一樣被傳入 filter 之中。
在實作題目的時候,上述的 code 完全可以一行不改,全部在調用端實作完成:
這下子不管面試官怎麼增加條件,我們的核心 code 都不會變,也可以很輕易的用 Lambda Expression 用極少的行數實作,也不會怕任何改動讓最終計算出現問題了。
Functional Programming 的正確使用方式
FP 雖然具有很大的彈性與擴展性,但通常初學者會建立一堆 lambda、把函數當成變數;並認為這麼寫就是 FP、就能增加程式品質——這是一個很大的謬誤。根據觀察,這背後的原因是因為初學者常常會忽略 2 個重點。
首先,是 FP 不只是一種語法,它是一種設計模式。FP 除了本身的語言特性之外,在 coding 的時候還需要遵循一些通用的法則,例如 Pure Function。遵循 FP 的實作法則,才能寫出低耦合、高內聚、可讀性極高的程式。
第二,FP 的 programmer 必須具備良好的抽象思維能力。具備高度抽象思維的 programmer,使用 FP 可能會比使用 OOP 有更大的自由度。當然,熟練後你也可以兩者混用。現在大部分的主流深度學習框架都是 FP+OOP 的產物,例如 TensorFlow、Torch 等。
如果要我說,進入阿里之後學到的最有用的 programming 技巧是什麼,我一定會說是 Functional Programming。但是 FP 並不是所有人都能夠輕鬆駕馭的,矛盾的是我一直無法精準地說出箇中差異在哪裡。直到最近理解了「抽象思維」的概念,才發現原來問題在於思維模式的欠缺。這篇文章希望能夠幫助所有的 programmer,在 programming 的道路越走越順暢。