有帮助的curry
我最近使用Ramda实现函数式组合的文章涉及到一个重要的主题。为了使用Ramda的方式做某种组合,我们需要这些方法被柯里化。
柯里化是啥,能吃吗?
实际上,柯里化
是从 Haskell 来的。Haskell是第一个研究这种技术的。(是的,他们用自己的名字命名了一个函数式编程语言。不仅如此,Curry的中间初始是B
,或者当然代表的是Brainf*ck)
柯里化是这么一个过程,它一个本来需要多个参数的方法转化为这样一种方法,这个方法在传递更少参数之后,返回一个新方法,返回的这个新方法会等待获取余下的参数。
基本的例子看起来像这样,这是一个普通的函数:
// 没有柯里化的版本
var formatName1 = function(first, middle, last) {
return first + middle + ' ' + last;
}
formatName1('John', 'Paul', 'Jones');
// => 'John Paul Jones' // (呃, 是音乐家或者海军上将?)
formatName1('John', 'Paul')
// => 'John Paul undefined'
但是一个柯里化的版本就更加有用了
// 柯里化的版本
var formatName2 = R.curry(function(first, middle, last) {
return first + ' ' + middel + ' ' + last;
});
formatName2('John', 'Paul', 'Jones');
// => 'John Paul Jones' (肯定是个音乐家)
var jp = formatName2('John', 'Paul');
// => 返回一个方法
jp('Jones');
// => 'John Paul Jones' (maybe this one's the admiral)
jp('Stevens');
//=> 'John Paul Stevens' (the Supreme Court Justice)
jp('Pontiff');
//=> 'John Paul Pontiff' (ok, so I cheated.)
jp('Ziller');
//=> 'John Paul Ziller' (magician, a wee bit fictional)
jp('Georgeandringo');
//=> 'John Paul Georgeandringo' (rockers)
或者
['Jones', 'Stevens', 'Eiller'].map(jp);
//=> ['John Paul Jones', 'John Paul Stevens', 'John Paul Ziller']
你也可以在多次这样传递
var james = formatName2('James');
// => return a function
james('Byron', 'Dean');
// => 'James Byron Dean'(rebel)
var je = james('Earl');
// 也会返回一个方法
je('Carter');
// => 'James Earl Carter'(president)
je('Jones');
// => 'James Earl Jones' (actor, Vader)
有些人会认为我们在做的应该叫"部分应用"更加合适,并且"柯里化"应该只是用在这种情况:返回的方法只接收一个参数,其他每个参数都使用一个独立的新方法处理直到所有必须的参数都被提供。这些人可以继续这样迷信(意译)。
补充: 其实这里就是使用闭包给方法绑定了数据,通过返回的函数在之后的调用中复用之前绑定的数据。这里使用的例子是一个经典的场景,叫做 惰性求值
。
无聊!对我有什么帮助吗?
下面是个更有意义的例子。如果你想求一个数字集合的和,你可以这么做:
// 普通JS
var add = function(a, b) {return a + b;};
var numbers = [1, 2, 3, 4, 5];
var sum = numbers.reduce(add, 0); // => 15
如果你想写一个通用的函数来计算数字列表的总数,你可能会这么写:
var total = function(list) {
return list.reduce(add, 0);
}
var sum = total(numbers); // => 15
在 Ramda里面, total
和 sum
非常相似。亦可以这样定义 sum
:
var sum = R.reduce(add, 0, numbers); // => 15
但是因为 reduce
是一个柯里化过的方法,但你不传最后一个参数的时候,就像在上面你自己定义的 total
函数一样:
// 在 Ramda 里
var total = R.reduce(add, 0); // 返回一个柯里化之后的方法
你仅仅拿到一个方法你可以这样调用:
var sum = total(numbers); // => 15
再次注意方法的定义和方法应用于数据上是多么相似
var total = R.reduce(add, 0); // => function
var sum = R.reduce(add, 0, numbers); // => 15
不在乎:我又不是个数学极客
所以你是一个web开发,是吧?你需要发一个AJAX请求到服务器?你需要使用 Promise?(起码我希望是这样)你需要操作返回的数据,过滤它,取得子集?或者你做服务器端开发?你异步查询SQL数据库,并且操作这些结果?
我能建议你做的最好的事情是你可以看看前端开发专家Jackson的精彩文章 为什么柯里化有帮助?。这是我看过最精彩的文章。如果你是个通过视频学习者,花半个小时看下Dr. Boolean的视频 Underscore,你做错了(不要在意标题,他没有花费他多时间实际吐槽工具库)。
真的去做。看看这些,它们解释得比我好,你已经看出来我多啰嗦了,巴拉巴拉。如果你已经看过这些,你可以跳过余下的章节。他们已经比我说得更好了。
该说的我都说了。
想象我们要获取像下面这样的数据
var data = {
result: "SUCCESS",
interfaceVersion: "1.0.3",
requested: "10/17/2013 15:31:20",
lastUpdated: "10/16/2013 10:52:39",
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
接收一个 membername
参数,然后从服务器(后者其他地方)获取数据,选择没有完成的会员,返回他们的ids,priorities, titles和due dates,根据due date排序。实际上,它返回一个应该在排序列表里面resolve的Promise。
如果你传递 "Scott" 给 getIncompleteTaskSummaries
,它可能会返回
[
{id: 110, title: "Rename everything",
dueDate: "2013-11-15", priority: "medium"},
{id: 104, title: "Do something",
dueDate: "2013-11-29", priority: "high"}
]
好啦,我们开始,下面的code看起来熟悉吗?
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')));
};
如果这样,那么柯里化会很适合你 。所有在这个代码块中提到的Ramda方法都被柯里化了。(实际上,所有超过一个参数的Ramda方法都被柯里化了,只有一些必要的例外。)在所有的情况下,柯里化让组合这些方法到一个优雅的块中变得简单。
让我们看看发生了什么。
get
(也用 prop
)是这样定义的:
ramda.get = curry(function(name, obj) {
return obj[name];
})
但是当我们像上面一样调用它,我们只是给了第一个参数 name
,正如我们讨论的,这意味着我们会得到一个等待通过第一个then方法传递obj参数的方法,就是下面这样
.then(R.get('tasks'))
可以认为是下面的简化写法
.then(function(data) {
return data.tasks;
})
下面是 propEq
,被定义为
ramda.propEq = curry(function(name, val, obj) {
return obj[name] === val;
});
所以当我们通过 username
和 membername
调用它的时候(后面的参数被作为参数传递给我们的方法),柯里化给我们返回了一个新方法,等价于
funciton(obj) {
return obj['username'] === membername;
}
membername
的值被绑定为传给我们的值。
这个方法之后被传给 filter
。
Ramda的 filter
方法就像在 Array.prototype
上的原生filter方法,但是函数签名是
ramda.filter = curry(function(predicate, list) {
});
所以我们已经柯里化过了,只需要传递 predicate
参数,并且前一步的tasks列表不需要。(我告诉过你所有的东西已经被柯里化了,没有吗?)
我们对于 propEq('complete', true) -> reject
跟 propEq('username', membername) ->
做了同样的事情。filter.Reject
跟 filter
是一样的,除了它会反转语义。它只保留哪些 predicate
返回 false
的值。
好了,你还在读吗?我们手指都累了。(真的要学学touch-type了)你真的不需要我来解释最后两行了,对吧?真的?你确定?好吧,好吧!是的!...是的,我说过我会的!
所以接下来我们看
R.pick(['id', 'dueDate', 'title', 'priority'])
pick
接收一个属性名数组和一个对象,并且返回一个新的对象,这个对象会把原来的属性拷贝过来。因为我们只传递了属性名,我们只要给它传递一个对象,就能得到一个会返回新对象的方法。这个方法被传递给 R.map
函数。跟 filter
一样,跟原生方法的版本工作方式相同,但是函数签名不同:
ramda.map = curry(function(fn, list) { / ... / });
这里再多嘴一下 -- 我已经说过我很乏味 -- 这个方法已经柯里化过了,因为我们只是传递了 pick
函数(柯里化后)的输出,而不是列表,then
会调用使用tasks列表调用这个方法。
好啦,记得坐在学校教室等待下课?钟表上的分针卡住了,秒针像是被糖蜜粘住了吗?老师成天唠叨一遍又一遍同样的事情。还记得吗?然后也许就是两分钟要结束前那一刻,结束尽在眼前:哈利路亚!我想这个例子完了之后我们就到了,就剩这个了:
.then(R.sortBy(R.get('dueDate')));
我们已经说过 get
方法了.就像这样柯里化之后,它返回一个方法,这个方法接受一个对象并且返回 dueDate
属性.我们把它传递给 sortBy
方法,这个方法接收一个列表并且基于方法返回值对这个列表元素排序.但是,等等,我们没有列表,对吧?当然没有,我们又柯里化了.但是当我们被 .then()
方法调用的时候,它会接收这个列表,把每个对象传递给 get
方法并且基于结果排序.
所以柯里化有多重要?
这个例子展示了 Ramda
的工具方法跟 Ramda
柯里化版本.也许柯里化并没有那么重要.让我们来在没有柯里化的情况下重写:
var getIncompleteTaskSummaries = function(membername) {
return fetchData()
.then(function(data) {
return R.get('tasks', data)
})
.then(function(tasks) {
return R.filter(function(task) {
return R.propEq('username', membername, task)
}, tasks)
})
.then(function(tasks) {
return R.reject(function(task) {
return R.propEq('complete', true, task);
}, tasks)
})
.then(function(tasks) {
return R.map(function(task) {
return R.pick(['id', 'dueDate', 'title', 'priority'], task);
}, tasks);
})
.then(function(abbreviatedTasks) {
return R.sortBy(function(abbrTask) {
return R.get('dueDate', abbrTask);
}, abbreviatedTasks);
});
};
我想其实是等价的.它也比原始的代码要好.Ramda
的工具方法有些,额,功用,甚至在柯里化的版本中.但是我不认为它比下面的版本可读性更好:
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')));
};
所以这就是我们柯里化的原因.
到这里课程结束了.
我确实警告过你.
下次,当我告诉你读其他人的文章而不是我的的时候,你要注意了,对吧?跟我写的比这些都有些过时了,但是他们仍然写得很好,并且你可以看看:
- 为什么柯里化有帮助, Hugh FD Jackson
- Underscore,你做错了, Dr. Boolean, aka Brian Lonsdorf
有个新的,我今天才看到.我们会看到它是否能够经受起时间的检验,但是当下它值得一读:
- 为了优雅重视回调, Gleb Bahmutov
一个小秘密
柯里化的确很强大,但是不足以让你的代码优雅.
有三个重要的元素:
- 之前我谈过函数组合.这在你把所有美妙的想法组装在一起而不想写丑陋的胶水代码时非常必要.
- 柯里化 很有用,既因为它需要支持组合,同时因为它把大量的样板代码去除了,正如你上面看到的.
- 一组工具方法操作有用的数据结构,例如对象列表.
Ramda 其中一个目标就是要通过方便的包装来提供所有这些.