摘要: JS函數語言程式設計入門。
- 原文:學會使用函數語言程式設計的程式設計師(第2部分)
- 作者:前端小智
Fundebug經授權轉載,版權歸原作者所有。
本系列的其他文章:
組合函式 (Function Composition)
作為程式設計師,我們是懶惰的。我們不想構建、測試和部署我們編寫的一遍又一遍的程式碼。我們總是試圖找出一次性完成工作的方法,以及如何重用它來做其他事情。
程式碼重用聽起來很棒,但是實現起來很難。如果程式碼業務性過於具體,就很難重用它。如時程式碼太過通用簡單,又很少人使用。所以我們需要平衡兩者,一種製作更小的、可重用的部件的方法,我們可以將其作為構建塊來構建更復雜的功能。
在函數語言程式設計中,函式是我們的構建塊。每個函式都有各自的功能,然後我們把需要的功能(函式)組合起來完成我們的需求,這種方式有點像樂高的積木,在程式設計中我們稱為 組合函式。
看下以下兩個函式:
var add10 = function(value) {
return value + 10;
};
var mult5 = function(value) {
return value * 5;
};
複製程式碼
上面寫法有點冗長了,我們用箭頭函式改寫一下:
var add10 = value => value + 10;
var mult5 = value => value * 5;
複製程式碼
現在我們需要有個函式將傳入的引數先加上 10 ,然後在乘以 5, 如下:
var mult5AfterAdd10 = value => 5 * (value + 10)
複製程式碼
儘管這是一個非常簡單的例子,但仍然不想從頭編寫這個函式。首先,這裡可能會犯一個錯誤,比如忘記括號。第二,我們已經有了一個加 10 的函式 add10 和一個乘以 5 的函式 mult5 ,所以這裡我們就在寫已經重複的程式碼了。
使用函式 add10,mult5 來重構 mult5AfterAdd10 :
var mult5AfterAdd10 = value => mult5(add10(value));
複製程式碼
我們只是使用現有的函式來建立 mult5AfterAdd10,但是還有更好的方法。
在數學中, f ∘ g 是函式組合,叫作“f 由 g 組合”,或者更常見的是 “f after g”。 因此 (f ∘ g)(x) 等效於f(g(x)) 表示呼叫 g 之後呼叫 f。
在我們的例子中,我們有 mult5 ∘ add10 或 “add10 after mult5”,因此我們的函式的名稱叫做 mult5AfterAdd10。由於Javascript本身不做函式組合,看看 Elm 是怎麼寫的:
add10 value =
value + 10
mult5 value =
value * 5
mult5AfterAdd10 value =
(mult5 << add10) value
複製程式碼
在 Elm 中 << 表示使用組合函式,在上例中 value 傳給函式 add10 然後將其結果傳遞給 mult5。還可以這樣組合任意多個函式:
f x =
(g << h << s << r << t) x
複製程式碼
這裡 x 傳遞給函式 t,函式 t 的結果傳遞給 r,函式 t 的結果傳遞給 s,以此類推。在Javascript中做類似的事情,它看起來會像 g(h(s(r(t(x))))),一個括號噩夢。
Point-Free Notation
Point-Free Notation就是在編寫函式時不需要指定引數的程式設計風格。一開始,這風格看起來有點奇怪,但是隨著不斷深入,你會逐漸喜歡這種簡潔的方式。
在 multi5AfterAdd10 中,你會注意到 value 被指定了兩次。一次在引數列表,另一次是在它被使用時。
// 這個函式需要一個引數
mult5AfterAdd10 value =
(mult5 << add10) value
複製程式碼
但是這個引數不是必須的,因為該函式組合的最右邊一個函式也就是 add10 期望相同的引數。下面的 point-free 版本是等效的:
// 這也是一個需要1個引數的函式
mult5AfterAdd10 =
(mult5 << add10)
複製程式碼
使用 point-free 版本有很多好處。
- 首先,我們不需要指定冗餘的引數。由於不必指定引數,所以也就不必考慮為它們命名。
- 由於更簡短使得更容易閱讀。本例比較簡單,想象一下如果一個函式有多個引數的情況。
天堂裡的煩惱
到目前為止,我們已經瞭解了組合函式如何工作以及如何通過 point-free 風格使函式簡潔、清晰、靈活。
現在,我們嘗試將這些知識應用到一個稍微不同的場景。想象一下我使用 add 來替換 add10:
add x y =
x + y
mult5 value =
value * 5
複製程式碼
現在如何使用這兩個函式來組合函式 mult5After10 呢?
我們可能會這樣寫:
-- 這是錯誤的!!!
mult5AfterAdd10 =
(mult5 << add) 10
複製程式碼
但這行不通。為什麼? 因為 add 需要兩個引數。
這在 Elm 中並不明顯,請嘗試用Javascript編寫:
var mult5AfterAdd10 = mult5(add(10)); // 這個行不通
複製程式碼
這段程式碼是錯誤的,但是為什麼?
因為這裡 add 函式只能獲取到兩個引數(它的函式定義中指定了兩個引數)中的一個(實際只傳遞了一個引數),所以它會將一個錯誤的結果傳遞給 mult5。這最終會產生一個錯誤的結果。
事實上,在 Elm 中,編譯器甚至不允許你編寫這種格式錯誤的程式碼(這是 Elm 的優點之一)。
我們再試一次:
var mult5AfterAdd10 = y => mult5(add(10, y)); // not point-free
複製程式碼
這個不是point-free風格但是我覺得還行。但是現在我不再僅僅組合函式。我在寫一個新函式。同樣如果這個函式更復雜,例如,我想使用一些其他的東西來組合mult5AfterAdd10,我真的會遇到麻煩。
由於我們不能將這個兩個函式對接將會出現函式組合的作用受限。這太糟糕了,因為函式組合是如此強大。
如果我們能提前給add函式一個引數然後在呼叫 mult5AfterAdd10 時得到第二個引數那就更好了。這種轉化我們叫做 柯里化。
柯里化 (Currying)
Currying 又稱部分求值。一個 Currying 的函式首先會接受一些引數,接受了這些引數之後,該函式並不會立即求值,而是繼續返回另外一個函式,剛才傳入的引數在函式形成的閉包中被儲存起來。待到函式被真正需要求值的時候,之前傳入的所有引數都會被一次性用於求值
上例我們在組合函式 mult5和 add(in) 時遇到問題的是,mult5 使用一個引數,add 使用兩個引數。我們可以通過限制所有函式只取一個引數來輕鬆地解決這個問題。我只需編寫一個使用兩個引數但每次只接受一個引數的add函式,函式柯里化就是幫我們這種工作的。
柯里化函式一次只接受一個引數。
我們先賦值 add 的第1個引數,然後再組合上 mult5,得到 mult5AfterAdd10 函式。當 mult5AfterAdd10 函式被呼叫的時候,add 得到了它的第 2 個引數。
JavaScript 實現方式如下:
var add = x => y => x + y
複製程式碼
此時的 add 函式先後分兩次得到第 1 個和第 2 個引數。具體地說,add函式接受單參x,返回一個也接受單參 y的函式,這個函式最終返回 x+y 的結果。
現在可以利用這個 add 函式來實現一個可行的 mult5AfterAdd10* :
var compose = (f, g) => x => f(g(x));
var mult5AfterAdd10 = compose(mult5, add(10));
複製程式碼
compose 有兩個引數 f 和 g,然後返回一個函式,該函式有一個引數 x,並傳給函式 f,當函式被呼叫時,先呼叫函式 g,返回的結果作為函式 f的引數。
總結一下,我們到底做了什麼?我們就是將簡單常見的add函式轉化成了柯里化函式,這樣add函式就變得更加自由靈活了。我們先將第1個引數10輸入,而當mult5AfterAdd10函式被呼叫的時候,最後1個引數才有了確定的值。
柯里化與重構(Curring and Refactoring)
函式柯里化允許和鼓勵你分隔複雜功能變成更小更容易分析的部分。這些小的邏輯單元顯然是更容易理解和測試的,然後你的應用就會變成乾淨而整潔的組合,由一些小單元組成的組合。
例如,我們有以下兩個函式,它們分別將輸入字串用單花括號和雙花括號包裹起來:
bracketed = function (str) {
retrun "{" + str + "}"
}
doubleBracketed = function (str) {
retrun "{{" + str + "}}"
}
複製程式碼
呼叫方式如下:
var bracketedJoe = bracketed('小智')
var doubleBracketedJoe = doubleBracketed('小智')
複製程式碼
可以將 bracket 和 doubleBracket 轉化為更變通的函式:
generalBracket = function( prefix , str ,suffix ) {
retrun prefix ++ str ++ suffix
}
複製程式碼
但每次我們呼叫 generalBracket 函式的時候,都得這麼傳參:
var bracketedJoe = generalBracket("{", "小智", "}")
var doubleBracketedJoe = generalBracket("{{", "小智", "}}")
複製程式碼
之前引數只需要輸入1個,但定義了2個獨立的函式;現在函式統一了,每次卻需要傳入3個引數,這個不是我們想要的,我們真正想要的是兩全其美。
因為生成小括號雙括號功能但一,重新調整一下 我們將 generalBracket 三個引數中的 prefix,str 各柯里化成一個函式,如下:
generalBracket = function( prefix ) {
return function( suffix ){
return function(str){
return prefix + str + suffix
}
}
}
複製程式碼
這樣,如果我們要列印單括號或者雙括號,如下:
// 生成單括號
var bracketedJoe = generalBracket('{')('}')
bracketedJoe('小智') // {小智}
// 生成雙括號
var bracketedJoe = generalBracket('{{')('}}')
bracketedJoe('小智') // {{小智}}
複製程式碼
常見的函式式函式(Functional Function)
函式式語言中3個常見的函式:Map,Filter,Reduce。
如下JavaScript程式碼:
for (var i = 0; i < something.length; ++i) {
// do stuff
}
複製程式碼
這段程式碼存在一個很大的問題,但不是bug。問題在於它有很多重複程式碼(boilerplate code)。如果你用命令式語言來程式設計,比如Java,C#,JavaScript,PHP,Python等等,你會發現這樣的程式碼你寫地最多。這就是問題所在。
現在讓我們一步一步的解決問題,最後封裝成一個看不見 for 語法函式:
先用名為 things 的陣列來修改上述程式碼:
var things = [1, 2, 3, 4];
for (var i = 0; i < things.length; ++i) {
things[i] = things[i] * 10; // 警告:值被改變!
}
console.log(things); // [10, 20, 30, 40]
複製程式碼
這樣做法很不對,數值被改變了!
在重新修改一次:
var things = [1, 2, 3, 4];
var newThings = [];
for (var i = 0; i < things.length; ++i) {
newThings[i] = things[i] * 10;
}
console.log(newThings); // [10, 20, 30, 40]
複製程式碼
這裡沒有修改things數值,但卻卻修改了newThings。暫時先不管這個,畢竟我們現在用的是 JavaScript。一旦使用函式式語言,任何東西都是不可變的。
現在將程式碼封裝成一個函式,我們將其命名為 map,因為這個函式的功能就是將一個陣列的每個值對映(map)到新陣列的一個新值。
var map = (f, array) => {
var newArray = [];
for (var i = 0; i < array.length; ++i) {
newArray[i] = f(array[i]);
}
return newArray;
};
複製程式碼
函式 f 作為引數傳入,那麼函式 map 可以對 array 陣列的每項進行任意的操作。
現在使用 map 重寫之前的程式碼:
var things = [1, 2, 3, 4];
var newThings = map(v => v * 10, things);
複製程式碼
這裡沒有 for 迴圈!而且程式碼更具可讀性,也更易分析。
現在讓我們寫另一個常見的函式來過濾陣列中的元素:
var filter = (pred, array) => {
var newArray = [];
for (var i = 0; i < array.length; ++i) {
if (pred(array[i]))
newArray[newArray.length] = array[i];
}
return newArray;
};
複製程式碼
當某些項需要被保留的時候,斷言函式 pred 返回TRUE,否則返回FALSE。
使用過濾器過濾奇數:
var isOdd = x => x % 2 !== 0;
var numbers = [1, 2, 3, 4, 5];
var oddNumbers = filter(isOdd, numbers);
console.log(oddNumbers); // [1, 3, 5]
複製程式碼
比起用 for 迴圈的手動程式設計,filter 函式簡單多了。最後一個常見函式叫reduce。通常這個函式用來將一個數列歸約(reduce)成一個數值,但事實上它能做很多事情。
在函式式語言中,這個函式稱為 fold。
var reduce = (f, start, array) => {
var acc = start;
for (var i = 0; i < array.length; ++i)
acc = f(array[i], acc); // f() 有2個引數
return acc;
});
複製程式碼
reduce函式接受一個歸約函式 f,一個初始值 start,以及一個陣列 array。
這三個函式,map,filter,reduce能讓我們繞過for迴圈這種重複的方式,對陣列做一些常見的操作。但在函式式語言中只有遞迴沒有迴圈,這三個函式就更有用了。附帶提一句,在函式式語言中,遞迴函式不僅非常有用,還必不可少。
原文:
medium.com/@cscalfani... medium.com/@cscalfani...
編輯中可能存在的bug沒法實時知道,事後為了解決這些bug,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具Fundebug。
你的點贊是我持續分享好東西的動力,歡迎點贊!
一個笨笨的碼農,我的世界只能終身學習!
更多內容請關注公眾號《大遷世界》!
關於Fundebug
Fundebug專注於JavaScript、微信小程式、微信小遊戲、支付寶小程式、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了9億+錯誤事件,付費客戶有Google、360、金山軟體、百姓網等眾多品牌企業。歡迎大家免費試用!