函數式程式語言
函數式程式語言是那些方便使用函數式程式設計範式的語言。簡單來說,如果具備函數式程式設計所需的特徵, 它就可以稱為函數式語言。在多數情況下,程式設計的風格實際上決定了一個程式是否是函數式的。
是什麼讓一個語言具有函數式特徵?
函數式程式設計無法用C語言來實現。函數式程式設計也無法用Java來實現(不包括那些透過大量變通手段實現的近似函數式程式設計)。 這些語言不包含支援函數式程式設計的結構。他們是純粹物件導向的、嚴格非函數式的語言。
同時,純函數語言也無法使用物件導向編程,例如Scheme、Haskell以及Lisp。
然而有些語言兩種模式都支援。 Python是個著名的例子,不過還有別的:Ruby,Julia,以及我們最感興趣的Javascript。 這些語言是如何支援這兩種差異如此之大的設計模式呢?它們包含兩種程式設計範式所需的特徵。 然而對Javascript來說,函數式的特徵似乎是被隱藏了。
但實際上,函數式語言所需要的比上述要多一些。到底函數式語言有什麼特徵呢?
特点 | 命令式 | 函数式 |
---|---|---|
编程风格 | 一步一步地执行,并且要管理状态的变化 | 描述问题和和所需的数据变化以解决问题 |
状态变化 | 很重要 | 不存在 |
执行顺序 | 很重要 | 不太重要 |
主要的控制流 | 循环、条件、函数调用 | 函数调用和递归 |
主要的操作单元 | 结构体和类对象 | 函数作为一等公民的对象和数据集 |
函數式語言的語法必須顧及到特定的設計模式,例如類型推斷系統和匿名函數。大體上,這個語言必須實現lambda演算。 而解釋器的求值策略必須是非嚴格、按需調用的(也稱為延遲執行),它允許不變資料結構和非嚴格、惰性求值。
译注:这一段用了一些函数式编程的专业词汇。lambda演算是一套函数推演的形式化系统(听起来很晕), 它的先决条件是内部函数和匿名函数。非严格求值和惰性求值差不多一个意思,就是并非严格地按照运算规则把所有元素先计算一遍, 而是根据最终的需求只计算有用的那一部分,比如我们要取有一百个元素的数组的前三项, 那惰性求值实际只会计算出一个具有三个元素是数组,而不会先去计算那个一百个元素的数组。
優點
當你最終掌握了函數式程式設計它將給你巨大的啟蒙。這樣的經驗會讓你後面的程式設計師生涯更上一個台階, 無論你是否真的會成為一個全職的函數式程式設計師。
不過我們現在不是在討論如何去學習冥想;我們正在探討如何去學習一個非常有用的工具,它將會讓你成為一個更好的程式設計師。
總的來說,什麼是使用函數式程式設計真正實際的優點呢?
更簡潔的程式碼
函數式程式設計更簡潔、更簡單、更小。它簡化了調試、測試和維護。
例如,我們需要這樣一個函數,它能將二維數組轉換為一維數組。如果只用命令式的技術,我們會寫成這樣:
function merge2dArrayIntoOne(arrays) { var count = arrays.length; var merged = new Array(count); var c = 0; for (var i = 0; i < count; ++i) { for (var j = 0, jlen = arrays[i].length; j < jlen; ++j) { merged[c++] = arrays[i][j]; } } return merged }
現在使用函數式技術,可以寫成這樣:
merge2dArrayIntoOne2 = (arrays) -> arrays.reduce (memo, item) -> memo.concat item , []
var merge2dArrayIntoOne2 = function(arrays) { return arrays.reduce( function(p,n){ return p.concat(n); }, []); };
译注:原著中代码有误,调用reduce函数时少了第二个参数空数组,这里已经补上。
這兩個函數有相同的輸入並傳回相同的輸出,但是函數式的範例更簡潔。
模組化
函數式程式設計強制把大型問題拆分成解決同樣問題的更小的情形,這就意味著程式碼會更加模組化。 模組化的程式有更清晰的描述,更容易調試,維護起來也更簡單。測試也會變得更加容易, 這是由於每個模組的程式碼都可以單獨檢測正確性。
多用性
由於其模組化的特性,函數式程式設計會有許多通用的輔助函數。你將會發現這裡面的許多函數可以在大量不同的應用中重複使用。
在後面的章節裡,許多最通用的函數將會被覆蓋到。然而,作為一個函數式程式設計師,你將不可避免地編寫自己的函數庫, 這些函數會被一次又一次地使用。例如一個用於在行間尋找設定檔的函數,如果設計好了也可以用來尋找Hash表。
減少耦合
耦合是程式裡模組間的大量相依性。由於函數式程式設計遵循編寫一等公民的、高階的純函數, 這使得它們對全域變數沒有副作用而彼此完全獨立,耦合極大程度上的減小了。 當然,函數會不可避免地相互依賴,但是改變一個函數不會影響其他的,只要輸入和輸出的一對一映射保持正確。
數學正確性
最後一點更理論一些。由於根植於lambda演算,函數式程式設計可以在數學上證明正確性。 這對一些研究者來說是一個巨大的優點,他們需要用程式來證明成長率、時間複雜度以及數學正確性。
我們來看看斐波那契數列。儘管它很少用於概念性證明以外的問題,但是用它來解釋這個概念非常好。 對一個斐波那契數列求值標準的方法是建立一個遞歸函數,像這樣:
fibonnaci(n) = fibonnaci(n-2) + fibonnaci(n–1)
還要加上一個一般情形:
return 1 when n < 2
這使得遞歸可以終止,並且讓遞歸呼叫堆疊裡的每一步從這裡開始累加。
以下列出詳細步驟
var fibonacci = function(n) { if (n < 2) { return 1; }else { return fibonacci(n - 2) + fibonacci(n - 1); } } console.log( fibonacci(8) ); // Output: 34
然而,在一個懶執行函數庫的輔助下,可以產生一個無窮大的序列,它是透過數學方程式來定義整個序列的成員的。 只有那些我們最終需要的成員最後才會被計算出來。
var fibonacci2 = Lazy.generate(function() { var x = 1, y = 1; return function() { var prev = x; x = y; y += prev; return prev; }; }()); console.log(fibonacci2.length()); // Output: undefined console.log(fibonacci2.take(12).toArray()); // Output: [1, 1, 2, 3, 5,8, 13, 21, 34, 55, 89, 144] var fibonacci3 = Lazy.generate(function() { var x = 1, y = 1; return function() { var prev = x; x = y; y += prev; return prev; }; }()); console.log(fibonacci3.take(9).reverse().first(1).toArray()); //Output: [34]
第二个例子明显更有数学的味道。它依赖Lazy.js函数库。还有一些其它这样的库,比如Sloth.js、wu.js, 这些将在第三章里面讲到。
我插几句:后面这个懒执行的例子放这似乎仅仅是来秀一下函数式编程在数学正确性上的表现。 更让人奇怪的是作者还要把具有相同内部函数的懒加载写两遍,完全没意义啊…… 我觉得各位看官知道这是个懒执就行了,不必深究。
非函数式世界中的函数式编程
函数式和非函数式编程能混合在一起吗?尽管这是第七章的主题,但是在我们进一步学习之前, 还是要弄明白一些东西。
这本书并没要想要教你如何严格地用纯函数编程来实现整个应用。这样的应用在学术界之外不太适合。 相反,这本书是要教你如何在必要的命令式代码之上使用纯函数的设计策略。
例如,你需要在一段文本中找出头四个只含有字母的单词,稚嫩一些的写法会是这样:
var words = [], count = 0; text = myString.split(' '); for (i=0; count < 4, i < text.length; i++) { if (!text[i].match(/[0-9]/)) { words = words.concat(text[i]); count++; } } console.log(words);
函数式编程会写成这样:
var words = []; var words = myString.split(' ').filter(function(x){ return (! x.match(/[1-9]+/)); }).slice(0,4); console.log(words);
如果有一个函数式编程的工具库,代码可以进一步被简化:
判斷一個函數是否能被寫成更函數式的方式是尋找循環和臨時變量,例如前面例子裡面的「words」和」count」變數。 我們通常可以用高階函數來取代迴圈和臨時變量,本章後面的部分將繼續探索。
Javascript是函數式程式語言嗎?
現在還有最後一個問題我們需要問自己,Javascript是函數式語言還是非函數式語言?
Javascript可以說是世界上最流行卻最沒有被理解的函數式程式語言。 Javascript是一種披著C外衣的函數式程式語言。 它的語法無疑和C比較像,這意味著它使用C語言的塊式語法和中綴語序。而且它是現存語言中名字起得最差勁的。 你不用去想像就可以看出來有多少人會因為Javascript和Java的關係而迷惑,就好像它的名字暗示了它會是什麼樣的東西! 但其實它和Java的共同點非常少。不過還真有一些要把Javascript強制弄成物件導向語言的主意, 例如Dojo、ease.js這些函式庫曾做了大量工作試圖抽象Javascript以使其適合物件導向程式設計。 Javascript來自於90年代那個滿世界都嚷嚷著物件導向的時代,我們被告知Javascript是一個物件導向語言是因為我們希望它是這樣, 但實際上它不是。
它的真實身分可以追溯到它的原型:Scheme和Lisp,兩個經典的函數式程式語言。 Javascript一直都是函數式程式語言。 它的函數是頭等公民,並且可以嵌套,它具有閉包和複合函數,它允許珂理化和monad。所有這些都是函數式程式設計的關鍵。 這裡另外還有一些Javascript是函數式語言的原因:
• Javascript的詞法包括了傳遞函數為參數的能力,具有類型推斷系統,支援匿名函數、高階函數、閉包等等。 這些特點對構成函數式程式設計的結構和行為至關重要。
• Javascript不是純粹物件導向語言,它的多數物件導向設計模式都是透過拷貝Prototype物件來完成的, 這是一個弱物件導向程式設計的模型。歐洲電腦製造商協會腳本(ECMAScript)-Javascript的正式形式與標準實作 -在4.2.1版本的規格裡有以下陳述:
「Javascript不具有像C 、Smalltalk、Java那樣的真正的類,但是支援創建物件的建構器。 一般來說,在基於類別的物件導向語言裡,狀態由實例承載,方法由類別承載,繼承只是針對結構和行為。
也就是說,Javascript的確不是一個純函數式語言。它缺乏惰性求值和內建的不可變數據。 這是由於大多數解釋器是按名調用,而不是按需調用。 Javascript由於其尾呼叫的處理方式也不太善於處理遞迴。 不過所有的這些問題都可以透過一些小小的注意事項來緩和。需要無窮序列和惰性求值的非嚴格求值可以透過一個叫Lazy.js的函式庫來實現。 不可變數只需要簡單的透過程式設計技巧就可以實現,不過它不是透過依賴語言層面來限製而是需要程式設計師自律。 尾遞歸消除可以透過一個叫Trampolining的方法來實現。這些問題將在第六章講解。
關於Javascript是函數式語言還是物件導向語言還是兩者皆是還是兩者皆非的爭論一直都很多,而且這些爭論還要繼續下去。
最後,函數式程式設計是透過巧妙的變化、組合、使用函數而實現編寫簡潔程式碼的方式。而Javascript為實現這些提供了很好的途徑。 如果你真要挖掘出Javascript全部的潛能,你必須學會如何將它當作函數式語言來使用。