JavaScript 專題系列第十六篇,講解函式組合,並且使用柯里化和函式組合實現 pointfree 模式
需求
我們需要寫一個函式,輸入 'kevin',返回 'HELLO, KEVIN'。
嘗試
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 將兩個函式組合成一個函式,讓程式碼從右向左執行,而不是由內而外執行,可讀性大大提升。這便是函式組合。
但是現在的 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 函式已經可以支援多個函式了,然而有了這個又有什麼用呢?
在此之前,我們先了解一個概念叫做 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專題系列目錄地址:github.com/mqyqingfeng…。
JavaScript專題系列預計寫二十篇左右,主要研究日常開發中一些功能點的實現,比如防抖、節流、去重、型別判斷、拷貝、最值、扁平、柯里、遞迴、亂序、排序等,特點是研(chao)究(xi) underscore 和 jQuery 的實現方式。
如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。