在閱讀關(guān)于 Currying(柯里化) , Partial Application(偏函數(shù)應(yīng)用) 和其他函數(shù)式編程技術(shù)之后,一些開(kāi)發(fā)人員不知道應(yīng)該什么時(shí)候使用這些方法;為什么要這樣使用?
在接下來(lái)的三篇系列文章中,我們將嘗試解決這個(gè)問(wèn)題,我們會(huì)嘗試,并向你展示如何在一個(gè)短小而現(xiàn)實(shí)的例子中用函數(shù)式編程的方式,解決這個(gè)問(wèn)題。
什么是函數(shù)式編程(Functional Programming)
在我們深入之前,讓我們花一點(diǎn)時(shí)間來(lái)回顧一下一些實(shí)用的函數(shù)式編程概念。
函數(shù)式編程把“function”作為重復(fù)使用的主要表達(dá)式。通過(guò)構(gòu)建專注于某個(gè)特定任務(wù)的小函數(shù),函數(shù)式編程使用合成(compose)來(lái)構(gòu)建更復(fù)雜的函數(shù) —— 這就是 Currying(柯里化) 和 Partial Application(偏函數(shù)應(yīng)用) 這樣的技術(shù)發(fā)揮作用的地方了。
函數(shù)式編程使用函數(shù)作為重復(fù)使用的聲明表達(dá)式,避免對(duì)狀態(tài)進(jìn)行修改,消除了副作用,并使用合成來(lái)構(gòu)建函數(shù)。
功能編程本質(zhì)上是用功能編程的!額外需要考慮的是:如避免狀態(tài)改變,無(wú)副作用的純函數(shù),消除循環(huán)支持遞歸是純函數(shù)式編程方法的一部分,用 Haskell 語(yǔ)言是這樣構(gòu)建的。
我們將重點(diǎn)介紹函數(shù)式編程的實(shí)用部分,以便我們可以在本系列博客文章中立即使用 Javascript 。
高階函數(shù)(Higher Order Functions) – JavaScript 中函數(shù)是“一等公民(first-class)”,這意味著我們可以將函數(shù)作為參數(shù)傳遞給其他函數(shù);也可以將函數(shù)作為其他函數(shù)的值返回。愚人碼頭注:以函數(shù)為參數(shù)或返回值的函數(shù)稱為“高階函數(shù)”。
裝飾器(Decorators) – 因?yàn)?JavaScript 中函數(shù)可以是高階函數(shù),所以我們可以創(chuàng)建函數(shù)來(lái)增加其他函數(shù)的行為 和/或 作為其他函數(shù)的參數(shù)。
合成(Composition) – 我們還可以創(chuàng)建由多個(gè)函數(shù)合成的函數(shù),創(chuàng)建鏈?zhǔn)降妮斎胩幚怼?
我們將介紹我們要使用的技術(shù),以便在需要時(shí)利用這些特性。這讓我們可以在上下文環(huán)境中引入它們,并使概念易于消化和理解。
讓我們開(kāi)始吧
OK,那我們打算怎么辦呢?
我們來(lái)看一個(gè)典型的例子,它需要處理從異步請(qǐng)求中獲取的一些數(shù)據(jù)。 在這種情況下,異步獲取數(shù)據(jù)采用了JSON格式,并包含了一個(gè)博客文章的摘要列表。
以下是我們將使用的異步獲取數(shù)據(jù):查看 Gist中的完整數(shù)據(jù) 和 示例數(shù)據(jù)。
// 異步獲取 JSON 數(shù)據(jù)的一條示例數(shù)據(jù)
var records = [
{
"id": 1,
"title": "Currying Things",
"author": "Dave",
"selfurl": "/posts/1",
"published": 1437847125528,
"tags": [
"functional programming"
],
"displayDate": "2015-07-25"
},
// ...
];
我們的需求:現(xiàn)在,假設(shè)我們想要顯示最近的文章(不超過(guò)一個(gè)月),按標(biāo)簽分組,按發(fā)布日期排序。讓我們思考一下我們需要做些什么
- 過(guò)濾掉一個(gè)月以前的文章(比如30天)。
- 通過(guò)他們的tags對(duì)文章進(jìn)行分組(這可能意味著如果他們有多個(gè)標(biāo)簽,則會(huì)顯示在兩個(gè)分組中。)
- 按發(fā)布日期排序每個(gè)標(biāo)簽列表,降序。
我們將在本系列文章中涵蓋上述每個(gè)需求,這篇文章從過(guò)濾開(kāi)始。
過(guò)濾數(shù)據(jù)
我們的第一步是過(guò)濾掉發(fā)布日期超過(guò) 30 天的文章記錄。由于函數(shù)式編程都是作為重用的主要表達(dá)式的函數(shù),所以讓我們構(gòu)建一個(gè)函數(shù)來(lái)封裝過(guò)濾列表的行為。
function filter(list, fn) {
return list.filter(fn);
}
有些朋友可能會(huì)問(wèn),“真的嗎?就這樣好了嗎?”
嗯,是的,沒(méi)有更多要寫(xiě)的了。
這個(gè)函數(shù)使用 predicate 斷言函數(shù)(fn) 來(lái)過(guò)濾一個(gè)數(shù)組(list),或許你會(huì)說(shuō),這可以通過(guò)直接調(diào)用list.filter(fn)來(lái)輕松實(shí)現(xiàn)。那么為什么不這樣做呢?
因?yàn)楫?dāng)我們將操作抽象成一個(gè)函數(shù)時(shí),我們就可以使用 Currying(柯里化) 來(lái)構(gòu)建一個(gè)更有用的函數(shù)。
Currying(柯里化) 是使用 N 個(gè)參數(shù)的函數(shù),返回一個(gè) N 個(gè)函數(shù)的嵌套系列,每個(gè)函數(shù)都采用 1 個(gè)參數(shù)。
有關(guān) Currying(柯里化) 概念的更多信息,請(qǐng)閱讀我以前的文章,并實(shí)現(xiàn) left -> right 的 currying(柯里化) 。
在這種情況下,我們將使用一個(gè)名為rightCurry()的函數(shù),該函數(shù)將函數(shù)的參數(shù)從右向左進(jìn)行柯里化。通常,一個(gè)普通curry()函數(shù)會(huì)將參數(shù)從左到右進(jìn)行柯里化。
這是我們的實(shí)現(xiàn),以及它在內(nèi)部使用的另一個(gè)實(shí)用函數(shù)flip()。
// 返回一個(gè)函數(shù),
// 該函數(shù)在調(diào)用時(shí)將參數(shù)的順序顛倒過(guò)來(lái)。
function flip(fn) {
return function() {
var args = [].slice.call(arguments);
return fn.apply(this, args.reverse());
};
}
// 返回一個(gè)新函數(shù),
// 從右到左柯里化原始函數(shù)的參數(shù)。
function rightCurry(fn, n) {
var arity = n || fn.length,
fn = flip(fn);
return function curried() {
var args = [].slice.call(arguments),
context = this;
return args.length >= arity ?
fn.apply(context, args.slice(0, arity)) :
function () {
var rest = [].slice.call(arguments);
return curried.apply(context, args.concat(rest));
};
};
}
通過(guò) currying(柯里化) ,我們可以創(chuàng)建一些函數(shù),允許我們創(chuàng)建新的、偏應(yīng)用的函數(shù),我們可以重用這些函數(shù)。 在我們這個(gè)例子中,我們將使用它來(lái)創(chuàng)建一個(gè)函數(shù),該函數(shù)部分應(yīng)用 predicate 斷言函數(shù)(fn)來(lái)進(jìn)行過(guò)濾列表的操作。
// 一個(gè)函數(shù),使用給定 predicate 斷言函數(shù) 過(guò)濾列表 var filterWith = rightCurry(filter);
這基本上與手動(dòng)調(diào)用二元的filter(list, fn)函數(shù)一樣,進(jìn)行相同的操作。
function filterWith(fn) {
return function(list) {
return filter(list, fn);
}
}
我們可以如下使用它嗎?
var list = [1,2,3,4,5,6,7,8,9,10];
// 創(chuàng)建一個(gè)偏應(yīng)用過(guò)濾器,獲取列表中的偶數(shù)
var justEvens = filterWith(function(n) { return n%2 == 0; });
justEvens(list);
// [2,4,6,8,10]
哇,可以!最初似乎是很多的工作; 但是我們從這個(gè)方法中得出的結(jié)論是:
- 使用 currying(柯里化) 創(chuàng)建一個(gè)通用的,可重用的函數(shù),filterWith(),可以在許多情況下使用它來(lái)創(chuàng)建更具體的列表過(guò)濾器
- 每當(dāng)我們得到一些數(shù)據(jù)時(shí),都可以懶惰地執(zhí)行這個(gè)新的過(guò)濾器。我們不能做到調(diào)用Array.prototype.filter的同時(shí),不使其立即對(duì)數(shù)據(jù)列表執(zhí)行操作
- 一個(gè)更具聲明性的API,有助于可讀性和理解
關(guān)于 predicate 斷言函數(shù)
我們的filterWith()函數(shù)需要一個(gè) predicate 斷言函數(shù),當(dāng)給定列表中的某個(gè)元素時(shí),它返回true或false,以確定是否應(yīng)該在新過(guò)濾的列表中返回該元素。
讓我們從一個(gè)更通用的比較函數(shù)開(kāi)始,它可以告訴我們一個(gè)給定的數(shù)是否大于或等于另一個(gè)數(shù)。
// 簡(jiǎn)單的使用 '>=' 比較
function greaterThanOrEqual(a, b) {
return a >= b;
}
我們文章的發(fā)布日期可以轉(zhuǎn)換成數(shù)字,時(shí)間戳格式(自Epoch以來(lái)的毫秒數(shù))這應(yīng)該可以正常工作。但是,用于過(guò)濾數(shù)組的斷言函數(shù)只能傳遞一個(gè)參數(shù)來(lái)檢查,而不是兩個(gè)。
那么,在需要一元函數(shù)的情況下,如何使我們的二元比較函數(shù)工作呢?
Currying(柯里化) 可以再次拯救我們!我們將使用它來(lái)創(chuàng)建一個(gè)函數(shù),該函數(shù)可以創(chuàng)建一元比較函數(shù)。
var greaterThanOrEqualTo = rightCurry(greaterThanOrEqual);
我們現(xiàn)在可以使用這個(gè)柯里化版本來(lái)創(chuàng)建一個(gè) predicate 斷言函數(shù),可以用于列表過(guò)濾,例如:
var list = [5,3,6,2,8,1,9,4,7],
// a unary comparison function to see if a value is >= 5
fiveOrMore = greaterThanOrEqualTo(5);
filterWith(fiveOrMore)(list);
// [5,6,8,9,7]
棒極了! 現(xiàn)在我們回到我們的示例,創(chuàng)建一個(gè) predicate 斷言函數(shù),具體解決我們?cè)鹊倪^(guò)濾掉發(fā)布在30天以前的文章了:
var thirtyDaysAgo = (new Date()).getTime() - (86400000 * 30),
within30Days = greaterThanOrEqualTo(thirtyDaysAgo);
var dates = [
(new Date('2015-07-29')).getTime(),
(new Date('2015-05-01')).getTime()
];
filterWith(within30Days)(dates);
// [1438128000000] - July 29th, 2015
到現(xiàn)在為止還挺好!
我們創(chuàng)建了一個(gè)可以輕松重用的 過(guò)濾 斷言函數(shù)。另外,因?yàn)槲覀兪褂玫氖呛瘮?shù)式方法,所以我們的代碼更具聲明性,易于遵循 – 它的讀取方式與工作原理完全相同??勺x性和維護(hù)是編寫(xiě)任何代碼時(shí)需要考慮的重要事情!
類型問(wèn)題…
呃,我們還有另一個(gè)問(wèn)題!我們的程序需要過(guò)濾的是一個(gè)對(duì)象列表,所以我們的 predicate 斷言函數(shù)將需要訪問(wèn)傳入的每一項(xiàng)的published屬性。
我們目前的 predicate 斷言函數(shù),within30Days()不能處理對(duì)象類型的參數(shù),只能處理具體的數(shù)值!讓我們用另一個(gè)函數(shù)來(lái)解決這個(gè)問(wèn)題吧?。阍谶@里看到一個(gè)模式了嗎?)
我們想重用我們現(xiàn)有的斷言函數(shù);但修改其參數(shù),以便它可以與我們的特定對(duì)象類型一起使用。這是一個(gè)新的實(shí)用函數(shù),讓我們通過(guò)修改其參數(shù)來(lái)擴(kuò)展現(xiàn)有的函數(shù)。
function useWith(fn /*, txfn, ... */) {
var transforms = [].slice.call(arguments, 1),
_transform = function(args) {
return args.map(function(arg, i) {
return transforms[i](arg);
});
};
return function() {
var args = [].slice.call(arguments),
targs = args.slice(0, transforms.length),
remaining = args.slice(transforms.length);
return fn.apply(this, _transform(targs).concat(remaining));
}
}
這是迄今為止最有趣的函數(shù)式實(shí)用工具函數(shù),并且?guī)缀跖c Ramda.js 庫(kù) 中相同名稱的函數(shù)相同。
useWith()返回一個(gè)修改原來(lái)函數(shù)(fn)的函數(shù),所以當(dāng)被調(diào)用時(shí),它將通過(guò)相應(yīng)的變換(txnfn)函數(shù)傳遞每個(gè)參數(shù)。如果在調(diào)用時(shí)比轉(zhuǎn)換函數(shù)有更多的參數(shù),那么剩下的參數(shù)將會(huì)以 “as is” 的形式傳遞。
讓我們用一個(gè)小例子來(lái)幫助解釋這個(gè)定義。簡(jiǎn)單地說(shuō),useWith()讓我們執(zhí)行以下操作:
function sum(a,b) { return a + b; }
function add1(v) { return v+1; }
var additiveSum = useWith(sum, add1, add1);
// 在總和接收 4 & 5 之前,
// 它們都首先通過(guò) 'add1()' 函數(shù)進(jìn)行轉(zhuǎn)換
additiveSum(4,5); // 11
當(dāng)我們調(diào)用additiveSum(4,5)時(shí),我們基本上可以得到以下調(diào)用棧:
-
additiveSum(4,5)
- add1(4) => 5
- add1(5) => 6
- sum(5, 6) => 11
我們可以使用useWith()來(lái)修改現(xiàn)有的 predicate 斷言函數(shù)來(lái)在對(duì)象類型上操作,而不是數(shù)值。首先,讓我們?cè)俅问褂?currying(柯里化) 來(lái)創(chuàng)建一個(gè)函數(shù),該函數(shù)允許我們創(chuàng)建 偏應(yīng)用的函數(shù),這些函數(shù)可以通過(guò)屬性名訪問(wèn)對(duì)象。
// 用于訪問(wèn)對(duì)象屬性的函數(shù)
function get(obj, prop) { return obj[prop]; }
// `get()` 的柯里化版本
var getWith = rightCurry(get);
現(xiàn)在我們可以使用getWith()作為變換函數(shù),從每個(gè)對(duì)象獲取.published日期,傳遞給用于過(guò)濾器(filter)的一元斷言函數(shù)。
// 我們修改后的斷言函數(shù)可以在
// record 對(duì)象的 `.published` 屬性上工作
var within30Days = useWith(greaterThanOrEqualTo(thirtyDaysAgo), getWith('published'));
我們來(lái)試試看一下測(cè)試數(shù)據(jù):
// 簡(jiǎn)單的對(duì)象數(shù)組
var dates = [
{ id: 1, published: (new Date('2015-07-29')).getTime() },
{ id: 2, published: (new Date('2015-05-01')).getTime() }
],
within30Days = useWith(greaterThanOrEqualTo(thirtyDaysAgo), getWith('published'));
// 獲取數(shù)組中 published(發(fā)布日期)在30天內(nèi)的任何對(duì)象
filterWith(within30Days)(dates);
// { id: 1, published: 1438128000000 }
準(zhǔn)備過(guò)濾!
好的,鑒于我們的第一個(gè)需求是保留最近30天內(nèi)的文章記錄,那么用我們的響應(yīng)數(shù)據(jù)來(lái)提供一個(gè)完整的實(shí)現(xiàn)。
filterWith(within30Days)(records);
// [
// { id: 1, title: "Currying Things", displayDate: "2015-07-25", ... },
// { id: 2, title: "ES6 Promises", displayDate: "2015-07-26", ... },
// { id: 7, title: "Common Promise Idioms", displayDate: "2015-08-06", ... },
// { id: 9, title: "Default Function Parameters in ES6", displayDate: "2015-07-06", ... },
// { id: 10, title: "Use More Parenthesis!", displayDate: "2015-08-26", ... },
// ]
在過(guò)去的30天里,我們現(xiàn)在有了一個(gè)新的文章列表??磥?lái)我們已經(jīng)滿足了第一個(gè)需求,并且有了一個(gè)良好的開(kāi)端。隨著我們的繼續(xù),我們將把函數(shù)式實(shí)用工具函數(shù)放在一個(gè)可以重用的庫(kù)中。
獲取源代碼:您可以看到這篇文章中 所寫(xiě)的源代碼 ,在單獨(dú)的functional.js文件中包含了我們所有的函數(shù)式實(shí)用工具函數(shù),在app.js文件中包含了我們的主應(yīng)用程序的邏輯。我們將后續(xù)的本系列的翁中添加這些代碼。
小結(jié)
我們已經(jīng)發(fā)現(xiàn)了一些函數(shù)式編程中的關(guān)鍵技術(shù),如 Currying(柯里化) 和 Partial Application(偏函數(shù)應(yīng)用) 以及可以使用它們的上下文。我們還發(fā)現(xiàn),專注于構(gòu)建小而有用的行數(shù),與函數(shù)式技術(shù)相結(jié)合,可以合成高階函數(shù),并實(shí)現(xiàn)更好的重用。有了這些基礎(chǔ),接下來(lái)的兩篇文章看起來(lái)就不那么令人生畏了。
哈爾濱品用軟件有限公司致力于為哈爾濱的中小企業(yè)制作大氣、美觀的優(yōu)秀網(wǎng)站,并且能夠搭建符合百度排名規(guī)范的網(wǎng)站基底,使您的網(wǎng)站無(wú)需額外費(fèi)用,即可穩(wěn)步提升排名至首頁(yè)。歡迎體驗(yàn)最佳的哈爾濱網(wǎng)站建設(shè)。
