为什么选择Ramda?
为什么选择Ramda?
当最近buzzdecafe向世界介绍Ramda时,有两种不同的回应。那些习惯于函数式编程技术的人——无论是JavaScript还是其他语言——大多反应是"酷"。他们可能对此感到兴奋或只是随意注意到另一个潜在的工具,但他们理解它的用途。
另一组人则以"什么?"作为回应。
对于不熟悉函数式编程的人来说,Ramda似乎没有任何作用。它的大多数重要功能已经被Underscore和LoDash这样的库覆盖了。
这些人是对的。如果你想继续使用相同的命令式和面向对象的编码风格,Ramda对你来说并没有太多可提供的。
然而,它确实提供了一种不同的编码方式:纯粹函数式编程语言中理所当然的风格——Ramda使你能够通过函数组合轻松构建复杂的逻辑。请注意,任何带有compose函数的库都会允许你进行函数组合;这里的关键点在于:"使其简单"。
让我们看看在Ramda中如何实现这一点。
"待办事项列表"似乎是Web框架比较的重要参考点,所以我们也用它们来说明我们的观点:假设我们想要过滤一个待办事项列表,移除所有已完成的项目。
使用内置的Array原型方法,我们可能会这样做:
// 普通JS
let pendingTasks = tasks.filter(task => !task.completed);
使用LoDash,它会更简单一些:
// LoDash
let pendingTasks = _.filter(tasks, {completed: false});
在这两种情况下,我们都得到了一个过滤后的任务列表。
在Ramda中,我们可以这样做:
let pending = R.filter(R.whereEq({completed: false}));
(注意:这里的代码仅创建了一个函数,还需要调用它并传入任务列表才能得到结果)
这正是重点所在。
因为我们现在有了一个可以轻松与其他函数组合使用的函数,以便对任意数据集进行操作。想象一下,如果我们有一个按用户分组待办事项的函数groupByUser,那么我们可以简单地创建一个新的函数:
let activeByUser = R.compose(groupByUser, pending);
该函数会选择未完成的任务并将它们按用户分组。
或者如果我们要对每个用户的待办事项按截止日期排序:
let sortUserTasks = R.compose(R.mapObj(R.sortBy(R.prop('dueDate'))), activeByUser);
一切都整合在一起了吗?
细心的读者可能已经注意到我们可以将上述所有步骤合并。既然我们的compose函数允许超过两个参数,为什么不一次性完成所有操作呢?
let sortUserTasks = R.compose(
R.mapObj(R.sortBy(R.prop('dueDate'))),
groupByUser,
R.filter(R.whereEq({completed: false}))
);
我的回答是,如果没有其他地方需要用到中间函数activeByUser和pending,这样做可能是合理的。但这会使调试变得更加困难,并且不会显著提高代码的可读性。
实际上,我认为我们应该反其道而行之。我们内部使用了一个相当复杂的部分,这部分本身可能是可复用的。也许我们应该这样做:
let sortByDueDate = R.sortBy(R.prop('dueDate'));
let sortUserTasks = R.compose(R.mapObj(sortByDueDate), activeByUser);
现在我们可以用sortByDueDate对任何包含可排序"dueDate"属性的对象集合进行排序。
哦,等等,有人提到我们应该按降序排列日期吗?
let sortByDueDateDesc = R.compose(R.reverse, sortByDueDate);
let sortUserTasks = R.compose(R.mapObj(sortByDueDateDesc), activeByUser);
如果你确定只希望按最近日期优先排序,可以将这些定义合并为一个sortByDueDateDesc。我个人倾向于保留两者,以防需要按升序或降序排序。但这取决于你。
数据在哪里?
我们仍然没有数据。这是怎么回事?没有数据的数据处理只是... 处理。你需要耐心等待。当你使用函数式编程时,你得到的只是形成管道的函数。一个函数将数据传递给下一个函数,依此类推,直到你需要的结果从另一端流出。
到目前为止,我们构建了这一系列函数:
pending: [Task] -> [Task]
sortByDueDate: [Task] -> [Task]
sortByDueDateDesc: [Task] -> [Task]
activeByUser: [Task] -> {String: [Task]}
sortUserTasks: {String: [Task]} -> {String: [Task]}
虽然我们使用了早期的函数来构建sortUserTasks,但它们都是独立有用的。不过,我们确实略过了某些细节。我只让你想象我们有groupByUser函数来构建activeByUser;实际上并没有看到它是如何构建的。我们是如何构建那个函数的?
这是一个技巧:
let groupByUser = R.groupBy(R.prop('username'));
groupBy函数类似于reduce,它根据每个元素上的某个键(这里是username)将列表分组为子列表。
(还在讨论新函数,还没有提到数据!并且看,更多有趣的新函数即将到来!)
但是,等等,还有更多
我们可以继续扩展这个例子。如果我们想从一个列表中选择前五个元素,我们可以使用Ramda的take函数。因此,要获取每个用户的前五个任务,我们可以这样做:
let topFiveUserTasks = R.compose(R.mapObj(R.take(5)), sortUserTasks);
然后我们可以减少返回的对象,只保留一部分属性,比如标题和截止日期。用户名在这个数据结构中显然是多余的,其他的属性可能是我们不想传递给其他系统的开销。
我们可以使用Ramda的project函数来完成这项工作:
let importantFields = R.project(['title', 'dueDate']);
let topDataAllUsers = R.compose(R.mapObj(importantFields), topFiveUserTasks);
我们一路走来创建的一些函数似乎确实可以在TODO应用程序中重复利用。其他函数可能只是占位符,可以与主要函数结合。因此,如果我们现在回顾一下,也许可以这样组合代码:
let pending = R.filter(R.whereEq({completed: false}));
let sortByDueDate = R.sortBy(R.prop('dueDate'));
let sortByDueDateDesc = R.compose(R.reverse, sortByDueDate);
let importantFields = R.project(['title', 'dueDate']);
let groupByUser = R.groupBy(R.prop('username'));
let activeByUser = R.compose(groupByUser, pending);
let topDataAllUsers = R.compose(R.mapObj(R.compose(importantFields, R.take(5), sortByDueDateDesc)), activeByUser);
好的,现在我可以看一些数据了吗?
是的。是的,你可以。
现在是时候将数据传递给我们的函数了。关键是这些函数都接受相同类型的数据,即待办事项数组。我们没有具体描述这些项目的结构,但我们知道它们至少必须具有以下属性:
completed: 布尔值dueDate: 字符串,格式为YYYY-MM-DDtitle: 字符串username: 字符串
因此,如果我们有一个任务数组,如何使用它?很简单:
let results = topDataAllUsers(tasks);
就这些?
所有这些准备工作,就为了这么一点?
恐怕就是这样。结果将会是一个类似下面的对象:
{
Michael: [
{dueDate: '2014-06-22', title: 'Integrate types with main code'},
{dueDate: '2014-06-15', title: 'Finish algebraic types'},
{dueDate: '2014-06-06', title: 'Types infrastructure'},
{dueDate: '2014-05-24', title: 'Separating generators'},
{dueDate: '2014-05-17', title: 'Add modulo function'}
],
Richard: [
{dueDate: '2014-06-22', title: 'API documentation'},
{dueDate: '2014-06-15', title: 'Overview documentation'}
],
Scott: [
{dueDate: '2014-06-22', title: 'Complete build system'},
{dueDate: '2014-06-15', title: 'Determine versioning scheme'},
{dueDate: '2014-06-09', title: 'Add `mapObj`'},
{dueDate: '2014-06-05', title: 'Fix `and`/`or`/`not`'},
{dueDate: '2014-06-01', title: 'Fold algebra branch back in'}
]
}