原文链接 - by Hugh FD Jackson
一个程序员的白日梦就是写一段代码, 然后毫不费劲地反复使用它. 这很生动, 因为你所写的表达了所需要的, 而复用则是因为… 你在复用. 而复用是坠吼的(你还有啥可指望的)…
柯里可以帮上忙
JavaScript中正常的函数调用都大致长这样:
var add = function(a, b) { return a + b }
add(1, 2) //= 3
一个函数接受几个参数, 返回一个值. 我可以少传几个参数(得到一个古怪的结果), 也可以传多了(多的参数通常被忽略):
add(1 ,2 ,'我没啥用') //= 3
add(1) //= NaN
而一个接受多个参数的柯里化的函数则相当于, 连续执行的多个只接受一个参数的函数. 例如, 柯里化的加法会像这样:
var curry = require('curry')
var add = curry(function(a, b){ return a + b })
var add100 = add(100)
add100(1) //= 101
一个接受多个参数的柯里化的函数会被写成:
var sum3 = curry(function(a, b, c){ return a + b + c })
sum3(1)(2)(3) //= 6
因为这么写在JavaScript的语法里有点丑, 柯里也允许你一次调用一个函数, 并传递多个参数:
var sum3 = curry(function(a, b, c){ return a + b + c })
sum3(1, 2, 3) //= 6
sum3(1)(2, 3) //= 6
sum3(1, 2)(3) //= 6
如果你不熟悉一些日常搞柯里化的语言(立即想到了Haskell), 也许柯里的优势还不会太明显. 而对我而言, 两个主要好处是:
来看个很简单的例子; 用map遍历一个集合获取每一项的id组成的集合:
var objects = [{ id: 1 }, { id: 2 }, { id: 3 }]
objects.map(function(o){ return o.id })
如果有人看不懂第二行的代码, 我就大发慈悲告诉你:
MAP 遍历 OBJECTS 来获取 IDS
这短短的一行里有很多屑代码; 以函数定义的形式存在. 我们来清理一下:
var get = curry(function(property, object){ return object[property] })
objects.map(get('id')) //= [1, 2, 3]
现在我们来讨论一下这个操作的真正逻辑 - map遍历这些对象, 获取id. 嘭, 我们在这个get函数中真正创建的是一个可以被部分配置的函数.
那如果我们仍然想重用’从一个对象组成的的数组获取id数组’功能的话该咋整? 先用一种很奈芙的做法来实现一下:
var getIDs = function(objects){
return objects.map(get('id'))
}
getIDs(objects) //= [1, 2, 3]
emmmm, 我们好像又从简洁优雅回到了乱七八糟. 还能做点什么抢救一下吗? 啊 - 把map也变成部分可用一个函数配置, 而让集合参数再等等怎么样?
var map = curry(function(fn, value){ return value.map(fn) })
var getIDs = map(get('id'))
getIDs(objects) //= [1, 2, 3]
我们可以看到, 如果基本的构造块是柯里化的函数, 我们就可以用它们轻松地创造出崭新的功能. 更令人兴奋的是, 代码读起来也就像你想表达的逻辑那样.
这么写的另一个优势在于它鼓励创造函数; 而不是方法. 虽然方法也很有用 - 可以实现多态, 以及可读性不错的代码 - 可他们也不是银弹, 例如在异步代码中就不太合适.
在这个简单的例子中, 我们从服务器get数据, 然后稍加处理. 数据大概长这样:
{
"user": "hughfdjackson",
"posts": [
{ "title": "why curry?", "contents": "..." },
{ "title": "prototypes: the short(est possible) story", "contents": "..." }
]
}
而你的任务是获取每个post的title. GOGOGO!
fetchFromServer()
.then(JSON.parse)
.then(function(data){ return data.posts })
.then(function(posts){
return posts.map(function(post){ return post.title })
})
好吧这不太公平, 我有点仓促. (而且代表你写了这段代码, 也许你可以更优雅的解决它, 有点跑题了)
因为promises( 或者回调, 随你喜欢 )链主要是在与函数打交道, 用map遍历一个服务器来的值, 而不先用一堆屑代码( 看上去和理解时 )把它包裹起来并不容易.
让我们再来一次, 这次用我们在上面已经定义过了的工具:
fetchFromServer()
.then(JSON.parse)
.then(get('posts'))
.then(map(get('title')))
这就是瘦逻辑, 轻松地表达意图; 不用柯里化的函数就没那么容易实现的.
其实这例子还不够复杂…
俺寻思下面这个截取自另一篇文章的一个例子可能会更直观一些:
getIncompleteTaskSummaries = function(membername) {
return fetchData()
.then(function(data) {
return data.tasks;
})
.then(function(tasks) {
var results = [];
for (var i = 0, len = tasks.length; i < len; i++) {
if (tasks[i].username == membername) {
results.push(tasks[i]);
}
}
return results;
})
.then(function(tasks) {
var results = [];
for (var i = 0, len = tasks.length; i < len; i++) {
if (!tasks[i].complete) {
results.push(tasks[i]);
}
}
return results;
})
.then(function(tasks) {
var results = [], task;
for (var i = 0, len = tasks.length; i < len; i++) {
task = tasks[i];
results.push({
id: task.id,
dueDate: task.dueDate,
title: task.title,
priority: task.priority
})
}
return results;
})
.then(function(tasks) {
tasks.sort(function(first, second) {
var a = first.dueDate, b = second.dueDate;
return a < b ? -1 : a > b ? 1 : 0;
});
return tasks;
});
};
=>
var getIncompleteTaskSummaries = function(membername) {
return fetchData()
.then(R.get('tasks'))
.then(R.filter(R.propEq('username', membername)))
.then(R.reject(R.propEq('complete', true)))
.then(R.map(R.pick(['id', 'dueDate', 'title', 'priority'])))
.then(R.sortBy(R.get('dueDate')));
};
这就很能说明问题了, 不是吗…