JS函數式編程【譯】2.1 函數式編程語言

函數式編程語言

函數式編程語言是那些方便於使用函數式編程範式的語言。簡單來講,若是具有函數式編程所需的特徵, 它就能夠被稱爲函數式語言。在多數狀況下,編程的風格實際上決定了一個程序是不是函數式的。 javascript

是什麼讓一個語言具備函數式特徵?

函數式編程沒法用C語言來實現。函數式編程也沒法用Java來實現(不包括那些經過大量變通手段實現的近似函數式編程)。 這些語言不包含支持函數式編程的結構。他們是純面向對象的、嚴格非函數式的語言。 html

同時,純函數語言也沒法使用面向對象編程,好比Scheme、Haskell以及Lisp。java

然而有些語言兩種模式都支持。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);
 });
};

這兩個函數具備一樣的輸入並返回相同的輸出,可是函數式的例子更簡潔。

模塊化

函數式編程強制把大型問題拆分紅解決一樣問題的更小的情形,這就意味着代碼會更加模塊化。 模塊化的程序具備更清晰的描述,更易調試,維護起來也更簡單。測試也會變得更加容易, 這是因爲每個模塊的代碼均可以單獨檢測正確性。

複用性

因爲其模塊化的特性,函數式編程會有許多通用的輔助函數。你將會發現這裏面的許多函數能夠在大量不一樣的應用裏重用。

在後面的章節裏,許多最通用的函數將會被覆蓋到。然而,做爲一個函數式程序員,你將會不可避免地編寫本身的函數庫, 這些函數會被一次又一次地使用。例如一個用於在行間查找配置文件的函數,若是設計好了也能夠用於查找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);

若是有一個函數式編程的工具庫,代碼能夠進一步被簡化:

var words = toSequence(myString).match(/[a-zA-Z]+/).first(4);

判斷一個函數是否能被寫成更加函數式的方式是尋找循環和臨時變量,好比前面例子裏面的「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那樣的真正的類,可是支持建立對象的構造器。 通常來講,在基於類的面嚮對象語言裏,狀態由實例承載,方法由類承載,繼承只是針對結構和行爲。 在EMACScript裏,狀態和方法由對象來承載,結構、行爲和狀態都會被繼承。」
  • Javascript是一個解釋型語言。Javascript的解釋器(有時被稱爲「引擎」)很是相似於Scheme的解釋器。 它們都是動態的,都有易於組合和傳輸的靈活的數據類型,都把代碼求值爲表達式塊,處理函數的方式也相似。

也就是說,Javascript的確不是一個純函數式語言。它缺少惰性求值和內建的不可變數據。 這是因爲大多數解釋器是按名調用,而不是按需調用。Javascript因爲其尾調用的處理方式也不太善於處理遞歸。 不過全部的這些問題均可以經過一些小的注意事項來緩和。須要無窮序列和惰性求值的非嚴格求值能夠經過一個叫Lazy.js的庫來實現。 不可變量只須要簡單的經過編程技巧就能夠實現,不過它不是經過依賴語言層面來限制而是須要程序員自律。 尾遞歸消除能夠經過一個叫Trampolining的方法實現。這些問題將在第六章講解。

關於Javascript是函數式語言仍是面嚮對象語言仍是二者皆是仍是二者皆非的爭論一直都不少,並且這些爭論還要繼續下去。

最後,函數式編程是經過巧妙的變化、組合、使用函數而實現編寫簡潔代碼的方式。並且Javascript爲實現這些提供了很好的途徑。 若是你真要挖掘出Javascript所有的潛能,你必須學會如何將它做爲一個函數式語言來使用。

相關文章
相關標籤/搜索