我對函數式編程的理解

漸漸地我們所熟悉的語言基本都或多或少地支持了函數式編程的特性,也越來越多地在各種場合聽到“函數式編程”。那麼究竟什麼是函數式編程呢?它會對我們帶來什麼影響?這些是我需要去探究的。看了一些書,查了一些資料,我覺得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 grades = new ArrayList();
//2. 遍歷找出所有女生
for (Student s : students) {
if (s.sex.equals("femail")) {
//3. 獲取該女生的成績
int grade = s.grade.
grades.add(grade);
}
}
//求所有女老師的年齡
//1. 定義一個列表,用來存放女老師的年齡
List ages = new ArrayList();
//2. 遍歷找出所有女老師
for (Teacher t : teachers) {
if (t.sex.equals("femail")) {
//3. 獲取女老師的年齡
ages.add(t.age);
}
}

用函數式編程的方式求解,可以這樣做:

//求所有女生的成績
List grades = students.stream().filter(s -> s.sex.equals("femail")).map(s -> {return s.grade}).collect(Collectors.toList());
//求所有女老師的年齡
List 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這的語言一開始就即支持函數式編程又支持面向對象編程,可以看出是可以共存和互補的。

  • Why Functional Programming Matters
  • 《函數式編程思維》
  • 函數式編程初探
  • Higher order functions, what are they?


分享到:


相關文章: