學會使用函數式編程的程序員(第1部分)

阿里雲最近在作活動,低至2折,有興趣能夠看看:
https://promotion.aliyun.com/...

爲了保證的可讀性,本文采用意譯而非直譯。
想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等着你!html

在這篇由多部分組成的文章中,接下來將介紹函數式編程的一些概念,這些概念對你學習函數式編程有所幫助。若是你已經懂了什麼是函數式編程,這能夠加深你的理解。前端

請不要着急。從這一點開始,花點時間閱讀並理解代碼示例。你甚至可能想在每節課結束後中止閱讀,以便讓你的觀點深刻理解,而後再回來完成。git

最重要的是你要理解。程序員

純函數(Purity)

圖片描述

所謂純函數,就是指這樣一個函數,對於相同的輸入,永遠獲得相同的輸出,它不依賴外部環境,也不會改變外部環境。若是不知足以上幾個條件那就是非純函數。github

下面是Javascript中的一個純函數示例:ajax

var z = 10;
function add(x, y) {
    return x + y;
}

注意,add 函數不涉及z變量。它不從z讀取,也不從z寫入,它只讀取xy,而後返回它們相加的結果。這是一個純函數。若是 add 函數確實訪問了變量z,那麼它就再也不是純函數了。正則表達式

請思考一下下面這個函數:sql

function justTen() {
    return 10;
}

若是函數justTen是純的,那麼它只能返回一個常量, 爲何?數據庫

由於咱們沒有給它任何參數。 並且,既然是純函數的,除了本身的輸入以外它不能訪問任何東西,它惟一能夠返回的就是常量。編程

因爲不帶參數的純函數不起做用,因此它們不是頗有用。因此justTen被定義爲一個常數會更好。

大多數有用的純函數必須至少帶一個參數。

考慮一下這個函數:

function addNoReturn(x, y) {
    var z = x + y
}

注意這個函數是不返回任何值。它只是把變量xy相加賦給變量z,但並無返回。

這個也是一個純函數,由於它只處理輸入。它確實對輸入的變量進行操做,可是因爲它不返回結果,因此它是無用的。

全部有用的純函數都必須返回一些咱們指望的結果。

讓咱們再次考慮第一個add函數:

圖片描述

注意 add(1, 2) 的返回結果老是 3。這不是奇怪的事情,只是由於 add 函數是純的。若是 add 函數使用了一些外部值,那麼你永遠沒法預測它的行爲。

在給定相同輸入的狀況下,純函數老是返回相同的結果。

因爲純函數不能改變任何外部變量,因此下面的函數都不是純函數:

writeFile(fileName);
updateDatabaseTable(sqlCmd);            
sendAjaxRequest(ajaxRequest);
openSocket(ipAddress);

全部這些功能都有反作用。當你調用它們時,它們會更改文件和數據庫表、將數據發送到服務器或調用操做系統以獲取套接字。它們不只對輸入操做同時也對輸出進行操做,所以,你永遠沒法預測這些函數將返回什麼。

純函數沒有反作用。

在Javascript、Java 和 c# 等命令式編程語言中,反作用無處不在。這使得調試很是困難,由於變量能夠在程序的任何地方更改。因此,當你有一個錯誤,由於一個變量在錯誤的時間被更改成錯誤的值,這不是很好。

此時,你可能會想,「我怎麼可能只使用純函數呢?」

函數式編程不能消除反作用,只能限制反作用。因爲程序必須與真實環境相鏈接,因此每一個程序的某些部分確定是不純的。函數式編程的目標是儘可能寫更多的純函數,並將其與程序的其餘部分隔離開來。

不可變性 (Immutability)

圖片描述

你還記得你第一次看到下面的代碼是何時嗎?

var x = 1;
x = x + 1;

教你初中數學的老師看到以上代碼,可能會問你,你忘記我給你教的數學了嗎? 由於在數學中,x 永遠不能等於x + 1。

但在命令式編程中,它的意思是,取x的當前值加1,而後把結果放回x中。

在函數式編程中,x = x + 1是非法的。因此這裏你能夠用數學的邏輯還記得在數式編程中這樣寫是不對的!

函數式編程中沒有變量。

因爲歷史緣由,存儲值的變量仍然被稱爲變量,但它們是常量,也就是說,一旦x取值,這個常量就是x返回的值。別擔憂,x 一般是一個局部變量,因此它的生命週期一般很短。但只要它還沒被銷燬,它的值就永遠不會改變。

下面是Elm中的常量變量示例,Elm是一種用於Web開發的純函數式編程語言:

addOneToSum y z =
    let
        x = 1
    in
        x + y + z

若是你不熟悉ml風格的語法,讓我解釋一下。addOneToSum 是一個函數,有兩個參數分別爲yz

let塊中,x被綁定到1的值上,也就是說,它在函數的生命週期內都等於1。當函數退出時,它的生命週期結束,或者更準確地說,當let塊被求值時,它的生命週期就結束了。

in塊中,計算能夠包含在let塊中定義的值,即 x,返回計算結果 x + y + z,或者更準確地說,返回 1 + y + z,由於 x = 1。

你可能又會想 :「我怎麼能在沒有變量的狀況下作任何事情呢?」

咱們想一下何時須要修改變量。一般會想到兩種狀況:多值更改(例如修改或記錄對象中的單個值)和單值更改(例如循環計數器)。

函數式編程使用參數保存狀態,最好的例子就是遞歸。是的,是沒有循環。「什麼沒有變量,如今又沒有循環? 」我討厭你! ! !」

哈哈,這並非說咱們不能作循環,只是沒有特定的循環結構,好比for, while, do, repeat等等。

函數式編程使用遞歸進行循環。

這裏有兩種方法能夠在Javascript中執行循環:

圖片描述

注意,遞歸是一種函數式方法,它經過使用一個結束條件 start (start + 1) 和調用本身 accumulator (acc + start) 來實現與 for 循環相同的功能。它不會修改舊的值。相反,它使用從舊值計算的新值。

不幸的是,這在 Javascript中 很難想懂,須要你花點時間研究它,緣由有二。第一,Javascript的語法相對其它高級語言比較亂,其次,你可能還不習慣遞歸思惟。

在Elm,它更容易閱讀,以下:

sumRange start end acc =
    if start > end then
        acc
    else
        sumRange (start + 1) end (acc + start)

它是這樣運行的:

圖片描述

你可能認爲 for 循環更容易理解。雖然這是有爭議的,並且更多是一個熟悉的問題,但非遞歸循環須要可變性,這是很差的。

在這裏,我尚未徹底解釋不變性的好處,可是請查看全局可變狀態部分,即爲何程序員須要限制來了解更多。

我尚未徹底解釋不可變性(Immutability)在這裏的好處,但請查看 爲何程序員須要限制的全局可變狀態部分 以瞭解更多信息。

不可變性的好處是,你讀取訪問程序中的某個值,但只有讀權限的,這意味着不用懼怕其餘人更改該值使本身讀取到的值是錯誤。

不可變性的還有一個好處是,若是你的程序是多線程的,那麼就沒有其餘線程能夠更改你線程中的值,由於該值是不可變,因此另外一個線程想要更改它,它只能從舊線程建立一個新值。

不變性能夠建立更簡單、更安全的代碼。

重構

讓咱們考慮一下重構,下面是一些Javascript代碼:

圖片描述

咱們之前可能都寫過這樣的代碼,隨着時間的推移,開始意識到這兩個函數其實是相同的,函數名稱,打印結果不太同樣而已。

咱們不該該複製 validateSsn 來建立 validatePhone,而是應該建立一個函數(共同的部分),經過參數形式實現咱們想要的結果。

重構後的代碼以下:

圖片描述

舊代碼參數中 ssnphone 如今用 value 表示,正則表達式 /^\d{3}-\d{2}-\d{4}$/ and /^\(\d{3}\)\d{3}-\d{4}$/ 由變量 regex. 表示。最後,消息「SSN」「電話號碼」 由變量 type 表示。

這個有相似的函數均可以使用這個函數來實現,這樣能夠保持代碼的整潔和可維護性。

高階函數

圖片描述

許多語言不支持將函數做爲參數傳遞,有些會支持但並不容易。

在函數式編程中,函數是一級公民。換句話說,函數一般是另外一個函數的值。

因爲函數只是值,咱們能夠將它們做爲參數傳遞。即便Javascript不是純函數語言,也可使用它進行一些功能性的操做。 因此這裏將上面的兩個函數重構爲單個函數,方法是將驗證合法性的函數做爲函數 parseFunc 的參數:

function validateValueWithFunc(value, parseFunc, type) {
  if (parseFunc(value))
    console.log('Invalid ' + type);
  else
    console.log('Valid ' + type);
}

像函數 parseFunc 接收一個或多個函數做爲輸入的函數,稱爲 高階函數

高階函數要麼接受函數做爲參數,要麼返回函數,要麼二者兼而有之。

如今能夠調用高階函數(這在Javascript中有效,由於Regex.exec在找到匹配時返回一個truthy值):

validateValueWithFunc('123-45-6789', /^\d{3}-\d{2}-\d{4}$/.exec, 'SSN');
validateValueWithFunc('(123)456-7890', /^\(\d{3}\)\d{3}-\d{4}$/.exec, 'Phone');
validateValueWithFunc('123 Main St.', parseAddress, 'Address');
validateValueWithFunc('Joe Mama', parseName, 'Name');

這比有四個幾乎相同的函數要好得多。

可是請注意正則表達式,這裏有點冗長了。簡化一下:

var parseSsn = /^\d{3}-\d{2}-\d{4}$/.exec;
var parsePhone = /^\(\d{3}\)\d{3}-\d{4}$/.exec;
validateValueWithFunc('123-45-6789', parseSsn, 'SSN');
validateValueWithFunc('(123)456-7890', parsePhone, 'Phone');
validateValueWithFunc('123 Main St.', parseAddress, 'Address');
validateValueWithFunc('Joe Mama', parseName, 'Name');

如今看起來好多了。如今,當要驗證一個電話號碼時,不須要複製和粘貼正則表達式了。

可是假設咱們有更多的正則表達式須要解析,而不只僅是 parseSsnparsePhone。每次建立正則表達式解析器時,咱們都必須記住在末尾添加 .exec,這很容易被忘記。

能夠經過建立一個返回exec 的高階函數來防止這種狀況:

function makeRegexParser(regex) {
    return regex.exec;
}
var parseSsn = makeRegexParser(/^\d{3}-\d{2}-\d{4}$/);
var parsePhone = makeRegexParser(/^\(\d{3}\)\d{3}-\d{4}$/);
validateValueWithFunc('123-45-6789', parseSsn, 'SSN');
validateValueWithFunc('(123)456-7890', parsePhone, 'Phone');
validateValueWithFunc('123 Main St.', parseAddress, 'Address');
validateValueWithFunc('Joe Mama', parseName, 'Name');

這裏,makeRegexParser採用正則表達式並返回exec函數,該函數接受一個字符串。validateValueWithFunc 將字符串 value 傳遞給 parse 函數,即exec。

parseSsnparsePhone 實際上與之前同樣,是正則表達式的 exec 函數。

固然,這是一個微小的改進,可是這裏給出了一個返回函數的高階函數示例。可是,若是makeRegexParser 要複雜得多,這種更改的好處是很大的。

下面是另外一個返回函數的高階函數示例:

function makeAdder(constantValue) {
    return function adder(value) {
        return constantValue + value;
    };
}

函數 makeAdder,接受參數 constantValue 並返回函數 adder,這個函數返回 constantValue 與它傳入參數相加結果。

下面是它的用法:

var add10 = makeAdder(10);
console.log(add10(20)); // 打印 30
console.log(add10(30)); // 打印 40
console.log(add10(40)); // 打印 50

咱們經過將常量10傳遞給 makeAdder 來建立一個函數 add10, makeAdder 返回一個函數,該函數將向返回的結果都加 10。

注意,即便在 makeAddr 返回以後,函數 adder 也能夠訪問變量 constantValue。 這裏能訪問到 constantValue 是由於存在閉包。

閉包機制很是重要,由於若是沒有它 ,返回函數的函數就不會有很大做用。因此必須瞭解它們是如何工做。

閉包

圖片描述

下面是一個使用閉包的函數的示例:

function grandParent(g1, g2) {
    var g3 = 3;
    return function parent(p1, p2) {
        var p3 = 33;
        return function child(c1, c2) {
            var c3 = 333;
            return g1 + g2 + g3 + p1 + p2 + p3 + c1 + c2 + c3;
        };
    };
}

在這個例子中,child 函數能夠訪問它自身的變量,函數 parent 函數能夠訪問它的自身變量和函數 grandParent 的變量。而函數 grandParent 只能訪問自身的變量。

下面是它的一個使用例子:

var parentFunc = grandParent(1, 2); // returns parent()
var childFunc = parentFunc(11, 22); // returns child()
console.log(childFunc(111, 222)); // prints 738
// 1 + 2 + 3 + 11 + 22 + 33 + 111 + 222 + 333 == 738

在這裏,parentFunc 保留了 parent 的做用域,由於 grandParent 返回 parent

相似地,childFunc 保留了 child 的做用域,由於 parentFunc 保留了 parent 的做用域,而 parent 的做用域 保留了 child 的做用域。

當一個函數被建立時,它在建立時做用域中的全部變量在函數的生命週期內都是可訪問的。一個函數只要還有對它的引用就存在。例如,只要childFunc 還引用 child 的做用域,child 的做用域就存在。

閉包具體還看看以前整理的一篇文章:我歷來不理解JavaScript閉包,直到有人這樣向我解釋它...

原文:
一、https://medium.com/@cscalfani...
二、https://medium.com/@cscalfani...

編輯中可能存在的bug無法實時知道,過後爲了解決這些bug,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具Fundebug

你的點贊是我持續分享好東西的動力,歡迎點贊!

交流

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

https://github.com/qq44924588...

我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!

關注公衆號,後臺回覆福利,便可看到福利,你懂的。

clipboard.png

相關文章
相關標籤/搜索