函式式編程

函式式編程

函式式編程是種編程方式,它將電腦運算視為函式的計算。函式程式語言最重要的基礎是λ演算(lambda calculus),而且λ演算的函式可以接受函式當作輸入(參數)和輸出(返回值)。

和指令式編程相比,函式式編程強調函式的計算比指令的執行重要。

和過程化編程相比,函式式編程里函式的計算可隨時調用。

基本介紹

  • 中文名:函式式編程
  • 外文名:Functional Programming
  • 類型:編程範式
  • 原理:將電腦運算視為函式的計算
  • 基礎:λ演算
  • 主要思想:把運算過程寫成嵌套函式調用
歷史,定義,例子,特性,優點,顧慮,

歷史

雖然 λ 演算並非設計來於計算機上執行,但可視為第一個函式式程式語言。1980年代末期,Haskell發布企圖集合很多函式式編程研究里的想法。

定義

簡單說,"函式式編程"是一種"編程範式"(programming paradigm),也就是如何編寫程式的方法論。
它屬於"結構化編程"的一種,主要思想是把運算過程儘量寫成一系列嵌套的函式調用。

例子

函式式編程中最古老的例子莫過於1958年被創造出來的LISP了,透過 LISP,可以用精簡的人力。較現代的例子包括Haskell、Clean、Erlang和Miranda等。

特性

閉包和高階函式
函式編程支持函式作為第一類對象,有時稱為閉包或者仿函式(functor)對象。實質上,閉包是起函式的作用並可以像對象一樣操作的對象。與此類似,FP 語言支持高階函式。高階函式可以用另一個函式(間接地,用一個表達式) 作為其輸入參數,在某些情況下,它甚至返回一個函式作為其輸出參數。這兩種結構結合在一起使得可以用優雅的方式進行模組化編程,這是使用 FP 的最大好處。
惰性計算
除了高階函式和仿函式(或閉包)的概念,FP 還引入了惰性計算的概念。在惰性計算中,表達式不是在綁定到變數時立即計算,而是在求值程式需要產生表達式的值時進行計算。延遲的計算使您可以編寫可能潛在地生成無窮輸出的函式。因為不會計算多於程式的其餘部分所需要的值,所以不需要擔心由無窮計算所導致的 out-of-memory 錯誤。一個惰性計算的例子是生成無窮 Fibonacci 列表的函式,但是對第n個Fibonacci 數的計算相當於只是從可能的無窮列表中提取一項。
遞歸
FP 還有一個特點是用遞歸做為控制流程的機制。例如,Lisp 處理的列表定義為在頭元素後面有子列表,這種表示法使得它自己自然地對更小的子列表不斷遞歸。
函式式編程具有五個鮮明的特點。
函式是"第一等公民"
所謂"第一等公民"(first class),指的是函式與其他數據類型一樣,處於平等地位,可以賦值給其他變數,也可以作為參數,傳入另一個函式,或者作為別的函式的返回值。
舉例來說,下面代碼中的print變數就是一個函式,可以作為另一個函式的參數。
var print = function(i){ console.log(i);};
[1,2,3].forEach(print);
只用"表達式",不用"語句"
"表達式"(expression)是一個單純的運算過程,總是有返回值;"語句"(statement)是執行某種操作,沒有返回值。函式式編程要求,只使用表達式,不使用語句。也就是說,每一步都是單純的運算,而且都有返回值。
原因是函式式編程的開發動機,一開始就是為了處理運算(computation),不考慮系統的讀寫(I/O)。"語句"屬於對系統的讀寫操作,所以就被排斥在外。
當然,實際套用中,不做I/O是不可能的。因此,編程過程中,函式式編程只要求把I/O限制到最小,不要有不必要的讀寫行為,保持計算過程的單純性。
沒有"副作用"
所謂"副作用"(side effect),指的是函式內部與外部互動(最典型的情況,就是修改全局變數的值),產生運算以外的其他結果。
函式式編程強調沒有"副作用",意味著函式要保持獨立,所有功能就是返回一個新的值,沒有其他行為,尤其是不得修改外部變數的值。
不修改狀態
上一點已經提到,函式式編程只是返回新的值,不修改系統變數。因此,不修改變數,也是它的一個重要特點。
在其他類型的語言中,變數往往用來保存"狀態"(state)。不修改變數,意味著狀態不能保存在變數中。函式式編程使用參數保存狀態,最好的例子就是遞歸。下面的代碼是一個將字元串逆序排列的函式,它演示了不同的參數如何決定了運算所處的"狀態"。
引用透明性
函式程式通常還加強引用透明性,即如果提供同樣的輸入,那么函式總是返回同樣的結果。就是說,表達式的值不依賴於可以改變值的全局狀態。這使您可以從形式上推斷程式行為,因為表達式的意義只取決於其子表達式而不是計算順序或者其他表達式的副作用。這有助於驗證正確性、簡化算法,甚至有助於找出最佳化它的方法。
副作用
副作用是修改系統狀態的語言結構。因為 FP 語言不包含任何賦值語句,變數值一旦被指派就永遠不會改變。而且,調用函式只會計算出結果 ── 不會出現其他效果。因此,FP 語言沒有副作用。

優點

1. 代碼簡潔,開發快速
函式式編程大量使用函式,減少了代碼的重複,因此程式比較短,開發速度較快。
Paul Graham在《黑客與畫家》一書中寫道:同樣功能的程式,極端情況下,Lisp代碼的長度可能是C代碼的二十分之一。
如果程式設計師每天所寫的代碼行數基本相同,這就意味著,"C語言需要一年時間完成開發某個功能,Lisp語言只需要不到三星期。反過來說,如果某個新功能,Lisp語言完成開發需要三個月,C語言需要寫五年。"當然,這樣的對比故意誇大了差異,但是"在一個高度競爭的市場中,即使開發速度只相差兩三倍,也足以使得你永遠處在落後的位置。"
2. 接近自然語言,易於理解
函式式編程的自由度很高,可以寫出很接近自然語言的代碼。
前文曾經將表達式(1 + 2) * 3 - 4,寫成函式式語言:
subtract(multiply(add(1,2), 3), 4)
對它進行變形,不難得到另一種寫法:
add(1,2).multiply(3).subtract(4)
這基本就是自然語言的表達了。再看下面的代碼,大家應該一眼就能明白它的意思吧:
merge([1,2],[3,4]).sort().search("2")
因此,函式式編程的代碼更容易理解。
3. 更方便的代碼管理
函式式編程不依賴、也不會改變外界的狀態,只要給定輸入參數,返回的結果必定相同。因此,每一個函式都可以被看做獨立單元,很有利於進行單元測試(unit testing)和除錯(debugging),以及模組化組合。
4. 易於"並發編程"
函式式編程不需要考慮"死鎖"(deadlock),因為它不修改變數,所以根本不存在"鎖"執行緒的問題。不必擔心一個執行緒的數據,被另一個執行緒修改,所以可以很放心地把工作分攤到多個執行緒,部署"並發編程"(concurrency)。
請看下面的代碼:
var s1 = Op1();
var s2 = Op2();
var s3 = concat(s1, s2);
由於s1和s2互不干擾,不會修改變數,誰先執行是無所謂的,所以可以放心地增加執行緒,把它們分配在兩個執行緒上完成。其他類型的語言就做不到這一點,因為s1可能會修改系統狀態,而s2可能會用到這些狀態,所以必須保證s2在s1之後運行,自然也就不能部署到其他執行緒上了。
多核CPU是將來的潮流,所以函式式編程的這個特性非常重要。
5. 代碼的熱升級
函式式編程沒有副作用,只要保證接口不變,內部實現是外部無關的。所以,可以在運行狀態下直接升級代碼,不需要重啟,也不需要停機。Erlang語言早就證明了這一點,它是瑞典愛立信公司為了管理電話系統而開發的,電話系統的升級當然是不能停機的。

顧慮

函式式編程常被認為嚴重耗費在CPU和存儲器資源。主因有二:
  • 早期的函式式程式語言實現時並無考慮過效率問題。
  • 有些非函式式程式語言為求提升速度,不提供自動邊界檢查或自動垃圾回收等功能。
惰性求值亦為語言如Haskell增加了額外的管理工作。

相關詞條

熱門詞條

聯絡我們