聊聊柯里化

僅以此文獻給個人學弟 誅諾_彌 ,並將逐風者的祝福送給他:
英雄,願你有一份無悔的愛情!react

什麼是柯里化

維基百科中有以下定義:git

在計算機科學中,柯里化(英語:Currying),是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數並且返回結果的新函數的技術。github

舉個例子,若是咱們實現一個三個數的加法函數,須要這麼實現:編程

function add(a, b, c) {
    return a + b + c;
}
add(1, 2, 3);   // 6

若是咱們將其柯里化(變換成接受一個單一參數的函數,而且返回接受餘下的參數並且返回結果的新函數),咱們的調用方式應該是這樣的。redux

add(1)(2)(3);   // 6

注意到在接受最後一個參數前,柯里化後的函數返回值都是函數,所以咱們實現以下:數組

function add(a) {
    return function (b) {
        return function (c) {
            return a + b + c;
        }
    }
}
add(1)(2)(3);   // 6

這樣咱們就實現了一個柯里化的add函數。因爲ES6中引入了箭頭函數,咱們能夠將上面的add實現成這樣:緩存

const add = a => b => c => a + b + c;

我想你大概知道爲何ES6要引入箭頭函數了。ide

柯里化的用途

就目前咱們知道的來看,柯里化僅僅是修改了一下函數參數的傳入方式或者說函數的調用方式,那麼有什麼用呢?函數式編程

考慮下面這個求兩數相除餘數的函數:函數

const modulo = divisor => dividend => dividend % divisor;
modulo(3)(9);   // 0

有個這個函數,咱們如今可以很輕鬆的寫出判斷一個數是奇數仍是偶數的函數:

const isOdd = modulo(2);

isOdd(6);   // 0
isOdd(5);   // 1

若是你沒有布爾值必定要用true、false的強迫症的話,這是一個很不錯的方案:)

接下來咱們來實現下面這個需求:給定一個由數字構成的數組,獲取裏面全部的奇數,怎麼辦呢?

先準備一個filter函數:

const filter = condition => arr => arr.filter(condition);

而後實現咱們的getTheOdd:

const getTheOdd = filter(isOdd);

getTheOdd([1, 2, 3, 4, 5]);     // [1, 3, 5]

到這裏我說一下個人理解,柯里化的函數有這樣一種能力:組合。將簡單的函數組合起來實現更復雜的功能,一方面可以更好的複用你的代碼,另外一方面可以培養一種對代碼拆解的直覺。

就像咱們在實現函數節流的時候(好比onscroll事件處理函數頻繁調用問題這樣的場景),經常會使用throttle包一下處理函數同樣,拆解代碼並進行組合每每能給咱們帶來更多的價值。

接下來咱們來看這個例子:

// 該函數接收一個數組,返回該數組元素倒序後的數組
const reverse = arr => arr.slice().reverse();
// 該函數接收一個數組,返回數組的第一個元素
const first = arr => arr[0];
// 基於上面兩個函數咱們能夠輕鬆實現獲取數組最後一個元素的函數
const last = arr => {
    const reversed = reverse(arr);
    return first(arr);
};

在提供函數式編程能力的JavaScript庫中,一般都會有一個用於組合的實現組合的函數:compose。咱們能夠用它來讓前一個例子更加函數式:

const compose = (...funcs) => {
    if (funcs.length === 0) {
        return arg => arg;
    }

    if (funcs.length === 1) {
        return funcs[0];
    }

    return funcs.reduce((a, b) => (...args) => a(b(...args)));
};

const last = compose(first, reverse);

從組合到傳播

考慮如下場景,咱們有一個執行很慢的函數(記爲slowFunc),咱們但願可以對它的值進行緩存,以此提升性能,怎麼辦呢?相信不少人都會想到memoize函數,咱們在下面給一個相對簡潔的實現,參考:https://github.com/reactjs/re...

const slowFuncWithCache = memoize(slowFunc);

function memoize(fn) {
    let lastArgs = null;
    let lastResult = null;

    return (...args) => {
        if (!isAllArgsEqual(args, lastArgs)) {
            lastArgs = args;
            lastResult = fn(...args);
        }
        return lastResult;
    };
}

function isAllArgsEqual(prev, next) {
    if (prev === null || next === null || prev.length !== next.length) {
        return false;
    }

    for (let i = 0; i < prev.length; i++) {
        if (prev[i] !== next[i]) {
            return false;
        }
    }

    return true;
}

回看咱們前面介紹compose時使用的例子。

const reverse = arr => arr.slice().reverse();

const first = arr => arr[0];

const last = compose(first, reverse);

若是咱們如今有一個需求,須要給last加一個緩存,怎麼辦?你可能直接就想到了這樣:

const last = memoize(compose(first, reverse));

不過其實咱們還能夠這樣:

const last = compose(memoize(first), memoize(reverse));

在last內層的first和reverse,在通過memoize處理得到緩存的能力後,也讓last得到了緩存的能力。這就是組合的傳播

毫不以爲這很熟悉?讓咱們回顧一下小學數學的知識:

a * (b + c) = a * b + a * c

組合的這種傳播特性給了咱們一種新的思路:若是咱們要實現一個大系統的數據緩存功能,不妨試着將系統中的每一步計算都加上緩存進行處理,若是每一步都進行了計算上的緩存,那麼最終這個系統必定是帶有緩存能力的。

結語

從認識柯里化,到利用柯里化的能力去組合咱們的更復雜的邏輯,再到把內部組合的出功能傳播到外層,這是一種化繁爲簡,以簡解繁的方法。在此借用otakustay前輩的一句話:嘗試始終將你的邏輯拆解到最簡,藉由組合和傳播,你會得到更多的可能性。

參考連接

相關文章
相關標籤/搜索