JavaScript專題之函數組合

JavaScript 專題系列第十六篇,講解函數組合,而且使用柯里化和函數組合實現 pointfree 模式git

需求

咱們須要寫一個函數,輸入 'kevin',返回 'HELLO, KEVIN'。github

嘗試

var toUpperCase = function(x) { return x.toUpperCase(); };
var hello = function(x) { return 'HELLO, ' + x; };

var greet = function(x){
    return hello(toUpperCase(x));
};

greet('kevin');

還好咱們只有兩個步驟,首先小寫轉大寫,而後拼接字符串。若是有更多的操做,greet 函數裏就須要更多的嵌套,相似於 fn3(fn2(fn1(fn0(x))))編程

優化

試想咱們寫個 compose 函數:數組

var compose = function(f,g) {
    return function(x) {
        return f(g(x));
    };
};

greet 函數就能夠被優化爲:服務器

var greet = compose(hello, toUpperCase);
greet('kevin');

利用 compose 將兩個函數組合成一個函數,讓代碼從右向左運行,而不是由內而外運行,可讀性大大提高。這即是函數組合。app

可是如今的 compose 函數也只是能支持兩個參數,若是有更多的步驟呢?咱們豈不是要這樣作:函數

compose(d, compose(c, compose(b, a)))

爲何咱們不寫一個帥氣的 compose 函數支持傳入多個函數呢?這樣就變成了:工具

compose(d, c, b, a)

compose

咱們直接抄襲 underscore 的 compose 函數的實現:測試

function compose() {
    var args = arguments;
    var start = args.length - 1;
    return function() {
        var i = start;
        var result = args[start].apply(this, arguments);
        while (i--) result = args[i].call(this, result);
        return result;
    };
};

如今的 compose 函數已經能夠支持多個函數了,然而有了這個又有什麼用呢?fetch

在此以前,咱們先了解一個概念叫作 pointfree。

pointfree

pointfree 指的是函數無須說起將要操做的數據是什麼樣的。依然是以最初的需求爲例:

// 需求:輸入 'kevin',返回 'HELLO, KEVIN'。

// 非 pointfree,由於提到了數據:name
var greet = function(name) {
    return ('hello ' + name).toUpperCase();
}

// pointfree
// 先定義基本運算,這些能夠封裝起來複用
var toUpperCase = function(x) { return x.toUpperCase(); };
var hello = function(x) { return 'HELLO, ' + x; };

var greet = compose(hello, toUpperCase);
greet('kevin');

咱們再舉個稍微複雜一點的例子,爲了方便書寫,咱們須要藉助在《JavaScript專題之函數柯里化》中寫到的 curry 函數:

// 需求:輸入 'kevin daisy kelly',返回 'K.D.K'

// 非 pointfree,由於提到了數據:name
var initials = function (name) {
    return name.split(' ').map(compose(toUpperCase, head)).join('. ');
};

// pointfree
// 先定義基本運算
var split = curry(function(separator, str) { return str.split(separator) })
var head = function(str) { return str.slice(0, 1) }
var toUpperCase = function(str) { return str.toUpperCase() }
var join = curry(function(separator, arr) { return arr.join(separator) })
var map = curry(function(fn, arr) { return arr.map(fn) })

var initials = compose(join('.'), map(compose(toUpperCase, head)), split(' '));

initials("kevin daisy kelly");

從這個例子中咱們能夠看到,利用柯里化(curry)和函數組合 (compose) 很是有助於實現 pointfree。

也許你會想,這種寫法好麻煩吶,咱們還須要定義那麼多的基礎函數……但是若是有工具庫已經幫你寫好了呢?好比 ramda.js

// 使用 ramda.js
var initials = R.compose(R.join('.'), R.map(R.compose(R.toUpper, R.head)), R.split(' '));

並且你也會發現:

Pointfree 的本質就是使用一些通用的函數,組合出各類複雜運算。上層運算不要直接操做數據,而是經過底層函數去處理。即不使用所要處理的值,只合成運算過程。

那麼使用 pointfree 模式究竟有什麼好處呢?

pointfree 模式可以幫助咱們減小沒必要要的命名,讓代碼保持簡潔和通用,更符合語義,更容易複用,測試也變得垂手可得。

實戰

這個例子來自於 Favoring Curry

假設咱們從服務器獲取這樣的數據:

var data = {
    result: "SUCCESS",
    tasks: [
        {id: 104, complete: false,            priority: "high",
                  dueDate: "2013-11-29",      username: "Scott",
                  title: "Do something",      created: "9/22/2013"},
        {id: 105, complete: false,            priority: "medium",
                  dueDate: "2013-11-22",      username: "Lena",
                  title: "Do something else", created: "9/22/2013"},
        {id: 107, complete: true,             priority: "high",
                  dueDate: "2013-11-22",      username: "Mike",
                  title: "Fix the foo",       created: "9/22/2013"},
        {id: 108, complete: false,            priority: "low",
                  dueDate: "2013-11-15",      username: "Punam",
                  title: "Adjust the bar",    created: "9/25/2013"},
        {id: 110, complete: false,            priority: "medium",
                  dueDate: "2013-11-15",      username: "Scott",
                  title: "Rename everything", created: "10/2/2013"},
        {id: 112, complete: true,             priority: "high",
                  dueDate: "2013-11-27",      username: "Lena",
                  title: "Alter all quuxes",  created: "10/5/2013"}
    ]
};

咱們須要寫一個名爲 getIncompleteTaskSummaries 的函數,接收一個 username 做爲參數,從服務器獲取數據,而後篩選出這個用戶的未完成的任務的 ids、priorities、titles、和 dueDate 數據,而且按照日期升序排序。

以 Scott 爲例,最終篩選出的數據爲:

[
    {id: 110, title: "Rename everything", 
        dueDate: "2013-11-15", priority: "medium"},
    {id: 104, title: "Do something", 
        dueDate: "2013-11-29", priority: "high"}
]

普通的方式爲:

// 初版 過程式編程
var fetchData = function() {
    // 模擬
    return Promise.resolve(data)
};

var getIncompleteTaskSummaries = function(membername) {
     return fetchData()
         .then(function(data) {
             return data.tasks;
         })
         .then(function(tasks) {
             return tasks.filter(function(task) {
                 return task.username == membername
             })
         })
         .then(function(tasks) {
             return tasks.filter(function(task) {
                 return !task.complete
             })
         })
         .then(function(tasks) {
             return tasks.map(function(task) {
                 return {
                     id: task.id,
                     dueDate: task.dueDate,
                     title: task.title,
                     priority: task.priority
                 }
             })
         })
         .then(function(tasks) {
             return tasks.sort(function(first, second) {
                 var a = first.dueDate,
                     b = second.dueDate;
                 return a < b ? -1 : a > b ? 1 : 0;
             });
         })
         .then(function(task) {
             console.log(task)
         })
};

getIncompleteTaskSummaries('Scott')

若是使用 pointfree 模式:

// 第二版 pointfree 改寫
var fetchData = function() {
    return Promise.resolve(data)
};

// 編寫基本函數
var prop = curry(function(name, obj) {
    return obj[name];
});

var propEq = curry(function(name, val, obj) {
    return obj[name] === val;
});

var filter = curry(function(fn, arr) {
    return arr.filter(fn)
});

var map = curry(function(fn, arr) {
    return arr.map(fn)
});

var pick = curry(function(args, obj){
    var result = {};
    for (var i = 0; i < args.length; i++) {
        result[args[i]] = obj[args[i]]
    }
    return result;
});

var sortBy = curry(function(fn, arr) {
    return arr.sort(function(a, b){
        var a = fn(a),
            b = fn(b);
        return a < b ? -1 : a > b ? 1 : 0;
    })
});

var getIncompleteTaskSummaries = function(membername) {
    return fetchData()
        .then(prop('tasks'))
        .then(filter(propEq('username', membername)))
        .then(filter(propEq('complete', false)))
        .then(map(pick(['id', 'dueDate', 'title', 'priority'])))
        .then(sortBy(prop('dueDate')))
        .then(console.log)
};

getIncompleteTaskSummaries('Scott')

若是直接使用 ramda.js,你能夠省去編寫基本函數:

// 第三版 使用 ramda.js
var fetchData = function() {
    return Promise.resolve(data)
};

var getIncompleteTaskSummaries = function(membername) {
    return fetchData()
        .then(R.prop('tasks'))
        .then(R.filter(R.propEq('username', membername)))
        .then(R.filter(R.propEq('complete', false)))
        .then(R.map(R.pick(['id', 'dueDate', 'title', 'priority'])))
        .then(R.sortBy(R.prop('dueDate')))
        .then(console.log)
};

getIncompleteTaskSummaries('Scott')

固然了,利用 compose,你也能夠這樣寫:

// 第四版 使用 compose
var fetchData = function() {
    return Promise.resolve(data)
};

var getIncompleteTaskSummaries = function(membername) {
    return fetchData()
        .then(R.compose(
            console.log,
            R.sortBy(R.prop('dueDate')),
            R.map(R.pick(['id', 'dueDate', 'title', 'priority'])
            ),
            R.filter(R.propEq('complete', false)),
            R.filter(R.propEq('username', membername)),
            R.prop('tasks'),
        ))
};

getIncompleteTaskSummaries('Scott')

compose 是從右到左依此執行,固然你也能夠寫一個從左到右的版本,可是從右向左執行更加可以反映數學上的含義。

ramda.js 提供了一個 R.pipe 函數,能夠作的從左到右,以上能夠改寫爲:

// 第五版 使用 R.pipe
var getIncompleteTaskSummaries = function(membername) {
    return fetchData()
        .then(R.pipe(
            ),
            R.prop('tasks'),
            R.filter(R.propEq('username', membername)),
            R.filter(R.propEq('complete', false)),
            R.map(R.pick(['id', 'dueDate', 'title', 'priority'])
            R.sortBy(R.prop('dueDate')),
            console.log,
        ))
};

專題系列

JavaScript專題系列目錄地址:https://github.com/mqyqingfeng/Blog

JavaScript專題系列預計寫二十篇左右,主要研究平常開發中一些功能點的實現,好比防抖、節流、去重、類型判斷、拷貝、最值、扁平、柯里、遞歸、亂序、排序等,特色是研(chao)究(xi) underscore 和 jQuery 的實現方式。

若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。

相關文章
相關標籤/搜索