在討論函數式編程(Functional Programming)的具體內容以前,咱們首先看一下函數式編程的含義。在維基百科上,函數式編程的定義以下:"函數式編程是一種編程範式。它把計算當成是數學函數的求值,從而避免改變狀態和使用可變數據。它是一種聲明式的編程範式,經過表達式和聲明而不是語句來編程。" (見 Functional Programming)html
函數式編程的思想在軟件開發領域由來已久。在衆多的編程範式中,函數式編程雖然出現的時間很長,可是在編程範式領域和整個開發社區中的流行度一直不溫不火。函數式編程有一部分狂熱的支持者,在他們眼中,函數式編程思想是解決各類軟件開發問題的終極方案;而另外的一部分人,則以爲函數式編程的思想並不容易理解,學習曲線較陡,上手起來也有必定的難度。大多數人更傾向於接受面向對象或是面向過程這樣的編程範式。這也是形成函數式編程範式一直停留在小衆階段的緣由。前端
這樣兩極化的反應,與函數式編程自己的特性是分不開的。函數式編程的思想脫胎於數學理論,也就是咱們一般所說的λ演算( λ-calculus)。一聽到數學理論,可能不少人就感受頭都大了。這的確是形成函數式編程的學習曲線較陡的一個緣由。如同數學中的函數同樣,函數式編程範式中的函數有獨特的特性,也就是一般說的無狀態或引用透明性(referential transparency)。一個函數的輸出由且僅由其輸入決定,一樣的輸入永遠會產生一樣的輸出。這使得函數式編程在處理不少與狀態相關的問題時,有着自然的優點。函數式編程的代碼一般更加簡潔,可是不必定易懂。函數式編程的解決方案中透露出優雅的美。java
函數式編程所涵蓋的內容很是普遍,從其背後的數學理論,到其中包含的基本概念,再到諸如 Haskell 這樣的函數式編程語言,以及主流編程語言中對函數式編程方式的支持,相關的專有第三方庫等。經過本系列的學習,你能夠了解到不少函數式編程相關的概念。你會發現不少概念均可以在平常的開發中找到相應的映射。好比作前端的開發人員必定據說太高階組件(high-order component),它就與函數式編程中的高階函數有着殊途同歸之妙。流行的前端狀態管理方案 Redux 的核心是 reduce 函數。庫 reselect 則是記憶化( memoization)的精妙應用。不少 Java 開發人員已經切實的體會到了 Java 8 中的 Lambda 表達式如何讓對流(Stream)的操做變得簡潔又天然。編程
近年來,隨着多核平臺和併發計算的發展,函數式編程的無狀態特性,在處理這些問題時有着其餘編程範式不可比擬的自然優點。無論是前端仍是後端開發人員,學習一些函數式編程的思想和概念,對於手頭的開發工做和之後的職業發展,都是大有裨益的。本系列雖然側重的是 Java 平臺上的函數式編程,可是對於其餘背景的開發人員一樣有借鑑意義。後端
下面介紹函數的基本概念。多線程
咱們先從數學中的函數開始談起。數學中的函數是輸入元素的集合到可能的輸出元素的集合之間的映射關係,而且每一個輸入元素只能映射到一個輸出元素。好比典型的函數 f(x)=x*x 把全部實數的集合映射到其平方值的集合,如 f(2)=4 和 f(-2)=4。函數容許不一樣的輸入元素映射到同一個輸出元素,可是每一個輸入元素只能映射到一個輸出元素。好比上述函數 f(x)=x*x 中,2 和-2 都映射到同一個輸出元素 4。這也限定了每一個輸入元素所對應的輸出元素是固定的。每一個輸入元素都必須被映射到某個輸出元素,也就是說函數能夠應用到輸入元素集合中的每一個元素。閉包
用專業的術語來講,輸入元素稱爲函數的參數(argument)。輸出元素稱爲函數的值(value)。輸入元素的集合稱爲函數的定義域(domain)。輸出元素和其餘附加元素的集合稱爲函數的到達域(codomain)。存在映射關係的輸入和輸出元素對的集合,稱爲函數的圖形(graph)。輸出元素的集合稱爲像(image)。這裏須要注意像和到達域的區別。到達域還可能包含除了像中元素以外的其餘元素,也就是沒有輸入元素與之對應的元素。併發
圖 1 表示了一個函數對應的映射關係(圖片來源於維基百科上的 Function 條目)。輸入集合 X 中的每一個元素都映射到了輸出集合 Y 中某個元素,即 f(1)=D、f(2)=C 和 f(3)=C。X 中的元素 2 和 3 都映射到了 Y 中的 C,這是合法的。Y 中的元素 B 和 A 沒有被映射到,這也是合法的。該函數的定義域是 X,到達域是 Y,圖形是 (1, D)、(2, C) 和 (3, C) 的集合,像是 C 和 D 的集合。app
咱們一般能夠把函數當作是一個黑盒子,對其內部的實現一無所知。只須要清楚其映射關係,就能夠正確的使用它。函數的圖形是描述函數的一種方式,也就是列出來函數對應的映射中全部可能的元素對。這種描述方式用到了集合相關的理論,對於圖 1 中這樣的簡單函數比較容易進行描述。對於包含輸入變量的函數,如 f(x)=x+1,須要用到更加複雜的集合邏輯。由於輸入元素 x 能夠是任何數,定義域是一個無窮集合,對應的輸出元素集合也是無窮的。要描述這樣的函數,用咱們下面介紹的 λ 演算會更加直觀。dom
λ 演算是數理邏輯中的一個形式系統,在函數抽象和應用的基礎上,使用變量綁定和替換來表達計算。討論 λ 演算離不開形式化的表達。在本文中,咱們儘可能集中在與編程相關的基本概念上,而不拘泥於數學上的形式化表示。λ 演算其實是對前面提到的函數概念的簡化,方便以系統的方式來研究函數。λ 演算的函數有兩個重要特徵:
對函數簡化以後,就能夠開始定義 λ 演算。λ 演算是基於 λ 項(λ-term)的語言。λ 項是 λ 演算的基本單元。λ 演算在 λ 項上定義了各類轉換規則。
λ 項由下面 3 個規則來定義:
全部的合法 λ 項,都只能經過重複應用上面的 3 個規則得來。須要注意的是,λ 項最外圍的括號是能夠省略的,也就是能夠直接寫爲 λx.M 和 MN。當多個 λ 項鍊接在一塊兒時,須要用括號來進行分隔,以肯定 λ 項的解析順序。默認的順序是左向關聯的。因此 MNO 至關於 ((MN)O)。在不出現歧義的狀況下,能夠省略括號。
重複應用上述 3 個規則就能夠獲得全部的λ項。把變量做爲λ項是重複應用規則的起點。λ 項 λx.M 定義的是匿名函數,把輸入變量 x 的值替換到表達式 M 中。好比,λx.x+1 就是函數 f(x)=x+1 的 λ 抽象,其中 x 是變量,M 是 x+1。λ 項 MN 表示的是把表達式 N 應用到函數 M 上,也就是調用函數。N 能夠是相似 x 這樣的簡單變量,也能夠是 λ 抽象表示的項。當使用λ抽象時,就是咱們一般所說的高階函數的概念。
在 λ 抽象中,若是變量 x 出如今表達式中,那麼該變量被綁定。表達式中綁定變量以外的其餘變量稱爲自由變量。咱們能夠用函數的方式來分別定義綁定變量(bound variable,BV)和自由變量(free variable,FV)。
對綁定變量來講:
對自由變量來講,相應的定義和綁定變量是相反的:
在 λ 項 λx.x+1 中,x 是綁定變量,沒有自由變量。在 λ 項 λx.x+y 中,x 是綁定變量,y 是自由變量。
在λ抽象中,綁定變量的名稱在某些狀況下是可有可無的。如 λx.x+1 和 λy.y+1 實際上表示的是一樣的函數,都是把輸入值加 1。變量名稱 x 或 y,並不影響函數的語義。相似 λx.x+1 和 λy.y+1 這樣的 λ 項在λ演算中被認爲是相等的,稱爲 α 等價(alpha equivalence)。
在 λ 項上能夠進行不一樣的約簡(reduction)操做,主要有以下 3 種。
α 變換(α-conversion)的目的是改變綁定變量的名稱,避免名稱衝突。好比,咱們能夠經過 α 變換把 λx.x+1 轉換成 λy.y+1。若是兩個λ項能夠經過α變換來進行轉換,則這兩個 λ 項是 α 等價的。這也是咱們上一節中提到的 α 等價的形式化定義。
對 λ 抽象進行 α 變換時,只能替換那些綁定到當前 λ 抽象上的變量。如 λ 抽象 λx.λx.x 能夠 α 變換爲 λx.λy.y 或 λy.λx.x,可是不能變換爲 λy.λx.y,由於二者的語義是不一樣的。λx.x 表示的是恆等函數。λx.λx.x 和 λy.λx.x 都是表示返回恆等函數的 λ 抽象,所以它們是 α 等價的。而 λx.y 表示的再也不是恆等函數,所以 λy.λx.y 與 λx.λy.y 和 λy.λx.x 都不是 α 等價的。
β 約簡(β-reduction)與函數應用相關。在討論 β 約簡以前,須要先介紹替換的概念。對於 λ 項 M 來講,M[x := N] 表示把 λ 項 M 中變量 x 的自由出現替換成 N。具體的替換規則以下所示。A、B 和 M 是 λ 項,而 x 和 y 是變量。A ≡ B 表示兩個 λ 項是相等的。
在進行替換以前,可能須要先使用 α 變換來改變綁定變量的名稱。好比,在進行替換 (λx.y)[y := x] 時,不能直接把出現的 y 替換成 x。這樣就改變了以前的 λ 抽象的語義。正確的作法是先進行 α 變換,把 λx.y 替換成 λz.y,再進行替換,獲得的結果是 λz.x。
替換的基本原則是要求在替換完成以後,原來的自由變量仍然是自由的。若是替換變量可能致使一個變量從自由變成綁定,須要首先進行 α 變換。在以前的例子中,λx.y 中的 x 是自由變量,而直接替換的結果 λx.x 把 x 變成了綁定變量,所以 α 變換是必須的。在正確的替換結果 λz.x 中,z 仍然是自由的。
β 約簡用替換來表示函數應用。對 ((λV.E) E′) 進行 β 約簡的結果就是 E[V := E′]。如 ((λx.x+1)y) 進行 β 約簡的結果是 (x+1)[x := y],也就是 y+1。
η 變換(η-conversion)描述函數的外延性(extensionality)。外延性指的是若是兩個函數當且僅當對全部參數的結果相同時,才被認爲是相等的。好比一個函數 F,當參數爲 x 時,它的返回值是 Fx。那麼考慮聲明爲 λy.Fy 的函數 G。函數 G 對於輸入參數 x,一樣返回結果 Fx。F 和 G 可能由不一樣的 λ 項組成,可是隻要 Fx=Gx 對全部的 x 都成立,那麼 F 和 G 是相等的。
以 F=λx.x 和 G=λx.(λy.y)x 來講,F 是恆等函數,而 G 則是在輸入參數 x 上應用恆等函數。F 和 G 雖然由不一樣的 λ 項組成,可是它們的行爲是同樣,本質上都是恆等函數。咱們稱之爲 F 和 G 是 η 等價的,F 是 G 的 η 約簡,而 G 是 F 的 η 擴展。F 和 G 互爲對方的 η 變換。
瞭解函數式編程的人可能據說過純函數和反作用等名稱。這兩個概念與引用透明性緊密相關。純函數須要具有兩個特徵:
對於第一個特徵,若是是從數學概念上抽象出來的函數,則很容易理解。好比 f(x)=x+1 和 g(x)=x*x 這樣的函數,都是典型的純函數。若是考慮到通常編程語言中出現的方法,則函數中不能使用靜態局部變量、非局部變量,可變對象的引用或 I/O 流。這是由於這些變量的值可能在不一樣的函數執行中發生變化,致使產生不同的輸出。第二個特徵,要求函數體中不能對靜態局部變量、非局部變量,可變對象的引用或 I/O 流進行修改。這就保證了函數的執行過程當中不會對外部環境形成影響。純函數的這兩個特徵缺一不可。下面經過幾個 Java 方法來具體說明純函數。
在清單 1 中,方法 f1 是純函數;方法 f2 不是純函數,由於引用了外部變量 y;方法 f3 不是純函數,由於使用了調用了產生反作用的 Counter 對象的 inc 方法;方法 f4 不是純函數,由於調用 writeFile 方法會寫入文件,從而對外部環境形成影響。
int f1(int x) { return x + 1;} int f2(int x) { return x + y;} int f3(Counter c) { c.inc(); return 0;} int f4(int x) { writeFile(); return 1;}
若是一個表達式是純的,那麼它在代碼中的全部出現,均可以用它的值來代替。對於一個函數調用來講,這就意味着,對於同一個函數的輸入參數相同的調用,均可以用其值來代替。這就是函數的引用透明性。引用透明性的重要性在於使得編譯器能夠用各類措施來對代碼進行自動優化。
隨着計算機硬件多核的普及,爲了儘量地利用硬件平臺的能力,併發編程顯得尤其重要。與傳統的命令式編程範式相比,函數式編程範式因爲其自然的無狀態特性,在併發編程中有着獨特的優點。以 Java 平臺來講,相信不少開發人員都對 Java 的多線程和併發編程有所瞭解。可能最直觀的感覺是,Java 平臺的多線程和併發編程並不容易掌握。這主要是由於其中所涉及的概念太多,從 Java 內存模型,到底層原語 synchronized 和 wait/notify,再到 java.util.concurrent 包中的高級同步對象。因爲併發編程的複雜性,即便是經驗豐富的開發人員,也很難保證多線程代碼不出現錯誤。不少錯誤只在運行時的特定狀況下出現,很難排錯和修復。在學習如何更好的進行併發編程的同時,咱們能夠從另一個角度來看待這個問題。多線程編程的問題根源在於對共享變量的併發訪問。若是這樣的訪問並不須要存在,那麼天然就不存在多線程相關的問題。在函數式編程範式中,函數中並不存在可變的狀態,也就不須要對它們的訪問進行控制。這就從根本上避免了多線程的問題。
做爲 Java 函數式編程系列的第一篇文章,本文對函數式編程作了簡要的概述。因爲函數式編程與數學中的函數密不可分,本文首先介紹了函數的基本概念。接着對做爲函數式編程理論基礎的λ演算進行了詳細的介紹,包括λ項、自由變量和綁定變量、α變換、β約簡和η變換等重要概念。最後介紹了編程中可能會遇到的純函數、反作用和引用透明性等概念。本系列的下一篇文章將對函數式編程中的重要概念進行介紹,包括高階函數、閉包、遞歸、記憶化和柯里化等。
原做者:成 富
原文連接: 函數式編程思想概論
原出處:IBM Developer