在閱讀關(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(柯里化) 。

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ò)濾列表的操作。

filterWith

// 一個(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è)。