我對函數語言程式設計的理解

zhang_sl發表於2018-10-23

漸漸地我們所熟悉的語言基本都或多或少地支援了函數語言程式設計的特性,也越來越多地在各種場合聽到“函數語言程式設計”。那麼究竟什麼是函數語言程式設計呢?它會對我們帶來什麼影響?這些是我需要去探究的。看了一些書,查了一些資料,我覺得John Hughes的Why Functional Programming Matters講得最高屋建瓴。本文的核心觀點和敘述架構基本上來自這篇文章。

什麼是函數語言程式設計

函數語言程式設計,顧名思義,在它的世界中,程式軟體就是一系列對引數進行操作的函式,這是函數語言程式設計的思想基石。例如我們一個普通的程式入口就是一個接受一些引數的main函式,而它本身也是由一些函式組成,而這些函式也是由更小的函式組成,一直到最簡單的函式。你看,我們可以從函式的角度去構建整個軟體。
所以函數語言程式設計是一種程式設計正規化,不在於具體的語言,具體的API。

函數語言程式設計的顯著特徵 – 不可變|無副作用|引用透明

在函數語言程式設計中,一個變數一旦被賦值,是不可改變的。沒有可變的變數,意味著沒有狀態。而中間狀態是導致軟體難以管理的一個重要原因,尤其在併發狀態下,稍有不慎,中間狀態的存在很容易導致問題。沒有中間狀態,也就能避免這類問題。無中間狀態,更抽象地說是沒有副作用。說的是一個函式只管接受一些入參,進行計算後吐出結果,除此以外不會對軟體造成任何其他影響,把這個叫做沒有副作用。因為沒有中間狀態,因此一個函式的輸出只取決於輸入,只要輸入是一致的,那麼輸出必然是一致的。這個又叫做引用透明。這些不同的名詞差不多都在講一個意思。

函數語言程式設計的目標 – 模組化

我們需要透過表象看到更深的抽象層次,例如結構化程式設計和非結構化程式設計的區別,從表面上看比較大的一個區別是結構化程式設計沒了“goto”語句。但更深層次是結構化程式設計使得模組化成為可能。像goto語句這樣的能力存在,雖然會帶來一定的便利,但是它會打破模組之間的界限,讓模組化變得不容易。而模組化有諸多好處,首先模組內部是更小的單一的邏輯,更容易程式設計;其次模組化有利於複用;最後模組化使得每個模組也更加易於測試。模組化是軟體成功的關鍵所在,模組化的本質是對問題進行分解,針對細粒度的子問題程式設計解決,然後把一個個小的解決方案整合起來,解決完整的問題。這裡就需要一個機制,可以將一個個小模組整合起來。函數語言程式設計有利於小模組的整合,有利於模組化程式設計。

將函式整合起來 – 高階函式(Higher-order Functions)

高階函式的定義。滿足以下其中一個條件即可稱為高階函式:

  • 接受一個或者多個函式作為其入參(takes one or more functions as arguments)
  • 返回值是一個函式 (returns a function as its result)

假如我們需要計算出學校中所有女生的成績,和所有女老師的年齡。傳統的程式設計方式我們是這樣做的:

//求所有女生的成績
//1. 定義一個列表,用來存放所有女生的成績
List<Integer> grades = new ArrayList();
//2. 遍歷找出所有女生
for (Student s : students) {
    if (s.sex.equals("femail")) {
        //3. 獲取該女生的成績
        int grade = s.grade.
        grades.add(grade);
    }
}

//求所有女老師的年齡
//1. 定義一個列表,用來存放女老師的年齡
List<Integer> ages = new ArrayList();

//2. 遍歷找出所有女老師
for (Teacher t : teachers) {
    if (t.sex.equals("femail")) {
        //3. 獲取女老師的年齡
        ages.add(t.age);
    }
}

用函數語言程式設計的方式求解,可以這樣做:

//求所有女生的成績
List<Integer> grades = students.stream().filter(s -> s.sex.equals("femail")).map(s -> {return s.grade}).collect(Collectors.toList());

//求所有女老師的年齡
List<Integer> ages = teachers.stream().filter(t -> t.sex.equals("femail")).map(t -> {return t.age}).collect(Collectors.toList());

例子中使用的是比較著名的高階函式,map, filter,此外常聽到的還有reduce。這些高階函式將迴圈給抽象了。map,filter裡面可以傳入不同的函式,操作不同的資料型別。但高階函式本身並不侷限於map,reduce,filter,滿足上述定義的都可以成為高階函式。高階函式像骨架一樣支起程式的整體結構,具體的實現則由作為引數傳入的具體函式來實現。因此,我們看到高階函式提供了一種能力,可以將普通函式(功能模組)整合起來,使得任一普通函式都能被靈活的替換和複用。

緩求值(Lazy Evaluations)

假如有一個函式g(f(x)),在常規的一旦知道x的值,則立即先求出f(x)的值,再將這個值代入到g()函式中。例如Java中寫

System.out.printl("Hello " + people.name);

//編譯後其實會變成
String s = "Hello " + people.name;
System.out.printl(s);

因為現在大部分傳統程式語言都是及早求值(eager evaluation)的。而在緩求值中,除非g()的結果需要被用到了,g()才會被觸發計算,而g()需要f()作為其輸入,f()把x代入開始計算。
緩求值的好處是:

  • 使昂貴的計算到必要時才會執行,優化效能
  • 可以建立無限大集合,只要一直接到請求,就一直輸出元素
  • 緩求值使得程式碼具備了巨大的優化潛能(例如TensorFlow用了這個思路)

但這與模組化有什麼關係呢?有關係!
假如全校學生的資料存放在一個巨大的檔案中,我們無法一次性將它load到記憶體裡面,但是我們又需要知道所有國慶節生日的同學名單。
套用g(f(x))的格式,我們需要filter(readFile(f)),按照及早求值的方式,先用readFile()把檔案內容讀取出來,然後在filter()裡面過濾,然而我們知道這個思路不可行,因為記憶體大小有限,無法一次性讀取。基於效能考慮,我們只好用別的方式,將readFile()和filter()寫在一個函式中,邊讀邊過濾,但是這樣就沒有模組化了。按照函數語言程式設計緩求值的方式,先執行到filter(),根據filter()函式的需要,readFile()去讀取對應的內容,由於用多少,讀多少,對記憶體沒有壓力,並且又很好地實現了兩個模組的分離。

結語

看待函數語言程式設計,如果只看到一些具體的特性,像map,reduce,緩求值等等,就會覺得不過如此,甚至覺得不過是把一些常用的邏輯整理了一下而已,那就錯過了函數語言程式設計的精彩。我們需要從函數語言程式設計的思想基石–基於函式構建軟體,以及函數語言程式設計對於模組化的益處,我們就能看到函數語言程式設計思想的魅力。
最後,函數語言程式設計會顛覆物件導向程式設計嗎?似乎蠻多人討論的。從我的理解,物件導向依然強大,在對現實世界的抽象上無可比擬。函數語言程式設計和麵向物件程式設計是不同的思路,有各自適用的場景在,也不是為了互相替代,是可以共存的。從像Java這樣典型的面嚮物件語言開始支援函數語言程式設計的特性,到Scala,Python這的語言一開始就即支援函數語言程式設計又支援物件導向程式設計,可以看出是可以共存和互補的。

參考資料


相關文章