聽說你想成為一名函數語言程式設計工程師(第二部分)
理解函數語言程式設計的概念是重要的第一步,也可能是最困難的一步。但不是說就一定得從概念起步。不妨換個適合的視角。
上一篇:第1部分
友情提示
請慢慢地閱讀程式碼,確保你能理解他們。本文的每一節都依賴於上一節的內容。
如果你過於著急,就可能錯過一些重要的細節。
重構
讓我們花點時間思考一下重構。這裡有一段 JavaScript 程式碼:
function validateSsn(ssn) { if (/^\d{3}-\d{2}-\d{4}$/.exec(ssn)) console.log('Valid SSN'); else console.log('Invalid SSN'); } function validatePhone(phone) { if (/^\(\d{3}\)\d{3}-\d{4}$/.exec(phone)) console.log('Valid Phone Number'); else console.log('Invalid Phone Number'); }
我們都寫過類似的程式碼,隨著時間的推移,我們會認識到這兩個函式實際基本上是相同的,只有一點點不同(用粗體 顯示)。
為了不使用拷貝貼上的方式從 validateSsn 建立 validatePhone ,我們需要建立一個函式,貼上內容並進修改,使之引數化。
在這個例子中,可以抽象出 值(value) 、 正則表示式(regex) 和列印的 訊息(message) (至少是輸出訊息的最後一部分)。
重構後的程式碼:
function validateValue(value, regex, type) { if (regex.exec(value)) console.log('Invalid ' + type); else console.log('Valid ' + type); }
舊程式碼中的引數 ssn 和 phone 現在由引數 value 傳入。
正則表示式 /^\d{3}-\d{2}-\d{4}$/ 和 /^(\d{3})\d{3}-\d{4}$/ 由引數 regex 傳入。
最後一,訊息的後面部分 ‘SSN’ 和 ‘Phone Number’ 由引數 type 傳入。
用一個函式比用兩個函式好得多,就更不用說代替三、四個,甚至十個函數了。這會讓你的程式碼整潔且易於維護。
比如說,如果存在 BUG,你只需要修改一個地方,而不是在整個程式碼庫中搜索這個函式可能被在哪些方被貼上修改過。
但是如果遇到下面這樣的情況該怎麼辦:
function validateAddress(address) { if (parseAddress(address)) console.log('Valid Address'); else console.log('Invalid Address'); } function validateName(name) { if (arseFullName(name)) console.log('Valid Name'); else console.log('Invalid Name'); }
這裡 parseAddress 和 parseFullName 都是需要一個 string 引數的函式,而且如果解析成功都返回 true 。
該如何重構呢?
我們可以像之前那樣,把 address 和 name 作為 value 傳入,而 'Address' 和 'Name' 作為 type ,然後在傳入正則表示式的地方傳入函式。
既然我們可以把函式作為引數傳入,那還有啥好說的……
高階函式
許多語言並不支援將函式作為引數傳遞。一些(語言)雖然支援,但過程繁瑣。
在函數語言程式設計中,函式便是該語言一等公民。換言之,一個函式只是另一種值的表現方式。
因為函式只是一些值而已,那麼我們便可把它們當做引數進行傳遞。
儘管Javascript不是純函式式語言,你依然可以用它做一些函式式操作。那麼如下便是最後兩個函式的重構結果,通過將那個名為 parseFunc 的 轉換函式 作為引數進行傳遞:
function validateValueWithFunc(value, parseFunc, type) { if (parseFunc(value)) console.log('Invalid ' + type); else console.log('Valid ' + type); }
我們的新函式就是一個 高階函式。
高階函式不僅可以將函式作為引數,還可以將函式作為結果返回。
現在我們可以呼叫我們的高階函式來實現之前四個函式的功能(這在Javascript中有效,因為當找到匹配時Regex.exec返回一個真值):
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');
那更好。 現在,當我們想要解析電話號碼時,我們不必複製和貼上正則表示式。
但是想象一下我們有更多的正則表示式來解析,而不僅僅是 parseSsn 和 parsePhone 。 每次我們建立一個正則表示式解析器時,我們都必須記住將 .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 **函式,該函式接受一個字串。 validateValueWithFuncwill 將字串 value 傳遞給parse函式,即 exec 。
parseSsn 和 parsePhone 實際上和以前一樣,是正則表示式的 exec 函式。
當然,這是一個微小的改進,但放到這裡是為了給出一個返回函式的高階函式的示例。
但是,如果 makeRegexParser 更復雜的話,你可以想象下做如此更改的好處。
這是返回函式的高階函式的另一個示例:
function makeAdder(constantValue) { return function adder(value) { return constantValue + value; }; }
這裡我們定義了 makeAdder ,它接收 constantValue 作為引數並返回 adder ——一個可以將傳遞給它的任意值加上給定常量的函式。
下面是它是如何被使用的示例:
var add10 = makeAdder(10); console.log(add10(20)); _// prints 30 _console.log(add10(30)); _// prints 40 _console.log(add10(40)); _// prints 50_
我們通過將常量10 傳遞給makeAdder 來建立一個add10 函式,該函式會返回一個將所有值都+10的函式。
請注意,即使在makeAddr 返回後,函式adder 也可以訪問constantValue 。那是因為當建立adder 時,constantValue 在其作用域之內。
這種行為非常重要,因為如果沒有它,返回函式的函式將不會非常有用。因此,重要的是我們要了解它們的工作方式以及此類行為的術語。
這種行為被稱為 Closure 。
Closures 閉包
這是一個使用閉包的函式的人為設計的例子:
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 的變數。
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
這裡,在 grandParent 返回 parent 之前, parentFunc 將在 parent 作用域內有效。
同樣地,在 parentFunc, 亦即 parent 返回 child 之前, childFunc 將在 child 作用域內有效。
建立函式時,在函式生命週期內,它可以訪問在其建立時其作用域內的所有變數。只要仍然存在對某函式的引用,該函式就是存在的。例如,只要 childFunc 仍引用 child ,那麼它的作用域就是存在的。
閉包是一個函式的作用域,它通過對該函式的引用保證其可見性。
請注意,在Javascript中,閉包是存在問題的,因為變數是可變的,即它們可以在封閉它們到呼叫返回函式的時間內改變值。
值得慶幸的是,函式式語言中的變數是不可變的,這規避了這種常見的錯誤和混淆源。
我的腦袋!!!!
到現在為止足夠了。
在本文的後續文章中,我將探討函式式組合、Currying、通用函式式函式(例如地圖、過濾器、摺疊等)等內容。