(譯) JavaScript中的組合函式

飛翔的大象發表於2019-03-08

簡介

lodash和underscore無處不在,但仍然有一種超級高效的方法,實際上只有那些趕時髦的人使用:組合。

我們將研究組合,並且深入瞭解為什麼這種方法會是你的程式碼更加具有可讀性,更易於維護和更加優雅。

基礎

我們將會使用lodash的一些函式,只是因為:

  • 我們不想編寫自己的簡單的實現,因為它們會分散我們關注的內容
  • lodash被廣泛使用,可以很容易的被underscore或其他的庫或者自己的實現替換

在我們深入研究一些基本的例子之前,讓我們回顧一下“組合”實際做了什麼,以及如果需要,我們如何實現我們自己的組合函式。

var compose = function(f, g) {
    return function(x) {
        return f(g(x));
    };
};
複製程式碼

這是最基本的實現。仔細看看上面的函式,你將會注意到傳入的函式實際上是從右向左呼叫的,意思是將右側函式的結果傳遞給它左側的函式。

現在仔細看看這段程式碼:

function reverseAndUpper(str) {
  var reversed = reverse(str);
  return upperCase(reversed);
}
複製程式碼

reverseAndUpper函式首先反轉給定的字串,然後變大寫。我們可以藉助基本的組合函式重寫以下程式碼:

var reverseAndUpper = compose(upperCase, reverse);
複製程式碼

現在我們可以使用reverseAndUpper

reverseAndUpper('test'); // TSET
複製程式碼

這相當於寫成:

function reverseAndUpper(str) {
  return upperCase(reverse(str));
}
複製程式碼

僅僅更加優雅、可維護和可重複使用。

快速組合函式的能力和建立資料管道的能力可以通過多種方式加以利用,從而可以在很長的管道中進行資料轉換。想象一下傳遞一個集合,對映集合,然後在管道的末尾但會一個最大值或者將給定字串轉換成布林值。組合是我們能夠輕鬆的連線多個函式用來構建更復雜的功能。

讓我們實現一個可以處理任意數量的函式以及任意數量的引數的非常靈活的組合函式,之前的組合函式只適用於兩個函式或者只接受第一個引數傳入,我們可以重寫組合函式如下:

var compose = function() {
  var funcs = Array.protoype.slice.call(arguments);

  return funcs.reduce(function(f,g) {
    return function() {
      return f(g.apply(this, arguments));
    };
  });
};
複製程式碼

這個組合函式能夠寫出像這樣的程式碼:

var doSometing = compose(upperCase, reverse, doSomethingInitial);

doSomething('foo', 'bar');
複製程式碼

存在大量的可以微我們實現組合的庫。我們的組合函式應該只有助於理解underscoresscoreunders組成函式中真正發生的事情,顯然具體實現在庫之間有所不同。它們大部分仍然在做同樣的事情:使它們各自的組成函式更加通用。現在我們已經知道什麼叫組合了,讓我們使用lodash中的 _.compose函式繼續下面的例子。

栗子

讓我們從一個很基礎的例子開始:

function notEmpty(str) {
    return ! _.isEmpty(str);
}
複製程式碼

函式notEmpty簡單的否定了_.isEmpty的結果。 我們可以使用lodash中的_.compose寫一個not函式來做同樣的事情。

function not(x) { return !x; }

var notEmpty = _.compose(not, _.isEmpty);
複製程式碼

現在我們可以在給定的任意引數呼叫notEmpty

notEmpty('foo'); // true
notEmpty(''); // false
notEmpty(); // false
notEmpty(null); // false
複製程式碼

第一個例子非常簡單,下一個會更高階:

findMaxForCollection會返回由idvalue屬性組成的給定物件集合的最大的id

function findMaxForCollection(data) {
    var items = _.pluck(data, 'val');
    return Math.max.apply(null, items);
}

var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}];

findMaxForCollection(data);
複製程式碼

上面的例子可以使用組合函式重寫。

var findMaxForCollection = _.compose(function(xs) { return Math.max.apply(null, xs); }, _.pluck);

var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}];

findMaxForCollection(data, 'val'); // 6
複製程式碼

我們可以在這重構很多。

_.pluck期待集合作為第一個引數,回撥函式作為第二個引數。如果我們想要部分應用_.pluck怎麼辦?這種情況下,我們可以使用柯里化來反轉引數。

function pluck(key) {
    return function(collection) {
        return _.pluck(collection, key);
    }
}
複製程式碼

我們的findMaxForCollection仍然需要更加精緻,我們可以建立我們的max函式。

function max(xs) {
    return Math.max.apply(null, xs);
}
複製程式碼

它可以使我們重寫組合函式來變得更加優雅:

var findMaxForCollection = _.compose(max, pluck('val'));

findMaxForCollection(data);
複製程式碼

通過編寫我們自己的pluck函式,我們可以用val部分地應用pluck。現在你可以明顯的爭辯,當lodash已經擁有更方便的_.pluck函式,為什麼要編寫自己的pluck方法?原因是_.pluck期望集合作為第一個引數,這不是我們想要的。通過反轉引數,我們可以將部分應用key,只需要在返回的函式上呼叫資料。

我們還可以進一步重寫pluck函式。lodash帶來了一個更簡單的方法:_.curry,這使我們能夠編寫如下的pluck函式:

function plucked(key, collection) {
    return _.pluck(collection, key);
}

var pluck = _.curry(plucked);
複製程式碼

我們只是想包裹原始的pluck函式,這樣我們就可以翻轉引數了。現在pluck仍簡單的返回一個函式,這麼長,直到所有的引數都被提供。讓我們看一下最終的程式碼:

function max(xs) {
    return Math.max.apply(null, xs);
}

function plucked(key, collection) {
    return _.pluck(collection, key);
}

var pluck = _.curry(plucked);

var findMaxForCollection = _.compose(max, pluck('val'));

var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}];

findMaxForCollection(data); // 6
複製程式碼

findMaxForCollection可以很容易的從右向左閱讀,這意味著集合先返回val的屬性,然後獲的所有給定值的最大值。

var findMaxForCollection = _.compose(max, pluck('val'));
複製程式碼

這使程式碼更具有可維護、可重用,並且更加優雅。我們將看一下最後的例子,以突出組合函式的優雅。

讓我們擴充套件前一個示例的資料,然後新增名為active的屬性,現在的資料如下:

var data = [{id: 1, val: 5, active: true},
            {id: 2, val: 6, active: false },
            {id: 3, val: 2, active: true }];
複製程式碼

我們有一個名為的getMaxIdForActiveItems(data)函式,它接收一組物件,過濾所有的active項,並從返回的過濾項中返回最大的id。

function getMaxIdForActiveItems(data) {
    var filtered = _.filter(data, function(item) {
        return item.active === true;
    });

    var items = _.pluck(filtered, 'val');
    return Math.max.apply(null, items);
}
複製程式碼

如果我們可以將上面的程式碼轉換成更優雅的內容怎麼辦?我們已經有了maxpluck函式,所以我們現在要做的是新增過濾:

var getMaxIdForActiveItems = _.compose(max, pluck('val'), _.filter);

getMaxIdForActiveItems(data, function(item) {return item.active === true; }); // 5
複製程式碼

_.filter有和_.pluck一樣的問題,這意味著我們不能將集合作為第一個引數來部分應用。我們可以通過包裹原生的filter實現,在filter上翻轉引數。

function filter(fn) {
    return function(arr) {
        return arr.filter(fn);
    };
}
複製程式碼

另一個改進是新增一個isActive函式,它只需要一個專案並檢查active標誌是否設定為true。

function isActive(item) {
    return item.active === true;
}
複製程式碼

我們可以在filter上部分應用isActive,使我們只能使用集合呼叫getMaxIdForActiveItems。

var getMaxIdForActiveItems = _.compose(max, pluck('val'), filter(isActive));
複製程式碼

現在我們需要傳遞的是資料:

getMaxIdForActiveItems(data); // 5
複製程式碼

這也使我們能夠輕鬆編寫一個函式,返回任何非active的最大id:

var isNotActive = _.compose(not, isActive);

var getMaxIdForNonActiveItems = _.compose(max, pluck('val'), filter(isNotActive));
複製程式碼

總結

組合函式可以很有趣,如前面的例子中所示,如果正確應用,可以產生更優雅和可重複使用的程式碼。

PS: 贊贊贊?,一個簡單的實現組合函式如下:

const doubleValue = (x) => x * 2;
const multiplyByFour = x => x + 4

const compose = (...funcs) => (value) => {
  return funcs.reduceRight((acc, func) => func(acc), value)
}

// (((f, g) => x) === f(g(x))
console.log(compose(doubleValue, multiplyByFour)(2)) // 12
複製程式碼

相關文章