当Buzzdecafe(Ramda库的主要贡献者)最近将Ramda介绍给世界时,有两种大相径庭的反应。那些习惯了函数式技术的人——不管是用JavaScript或其余语言——大多会回答“酷”。他们可能对此感到兴奋,或者只是随便地注意到另外一个潜在的工具,但他们理解它的用途。node
第二种反应是:“哈?”npm
对于那些不习惯于函数式编程的人来讲,ramda彷佛毫无用处。它的大部分主要功能已经被诸如Underscore和lodash之类的库所实现了。编程
这些人是对的。若是你想用你一直使用的命令式和面向对象风格来编写代码,Ramda没有更多的功能提供给你。数组
然而,它确实提供了一种不一样的编码风格,这种风格在纯函数式编程语言中被认为是理所固然的:Ramda经过函数组合的方式使构建复杂逻辑变得简单。请注意,任何具备compose函数的库都容许您进行函数组合;但Ramda真正的要点是:“使其简单化”。浏览器
让咱们看看Ramda是如何运做的。缓存
Web框架老是拿“todolist”当作事例,所以咱们也用它作事例:想象这样一个场景,筛选todolist以删除全部已完成的项。bash
使用内置的数组原型方法,咱们能够这样作:数据结构
// Plain JS
var incompleteTasks = tasks.filter(function(task) {
return !task.complete;
});
复制代码
使用LoDash,会简单一点:框架
// Lo-Dash
var incompleteTasks = _.filter(tasks, {complete: false});
复制代码
上面两种状况,咱们都会获得一个通过过滤的任务列表。编程语言
如今使用Ramda,咱们能够这样作:
var incomplete = R.filter(R.where({complete: false});
复制代码
注意到什么东西不见了吗?任务列表tasks没有了。这个ramda代码只是给了咱们一个函数。
为了获得结果,咱们仍然须要用任务列表tasks来调用它。
var incompleteTasks = incomplete(tasks);
复制代码
这就是重点所在。
由于咱们如今有了一个函数,咱们能够很容易地将它与其余函数结合起来,而后再对数据进行操做。假设咱们有一个函数groupbyuser,它按用户对todo项进行分组。而后咱们能够简单地建立一个新的函数:
var activeByUser = R.compose(groupByUser, incomplete);
复制代码
上面代码实现了选择未完成的任务并按用户分组。
若是不使用Ramda的compose,而是本身手动实现函数组合,则须要写一个这样的函数:
// (if created by hand)
var activeByUser = function(tasks) {
return groupByUser(incomplete(tasks));
};
复制代码
使用Ramda的好处就是不用每次手动实现函数组合。组合是函数式编程的关键技术之一。让咱们多考虑一些状况。若是咱们须要按截止日期对每一个用户的todolist进行排序呢?
var sortUserTasks = R.compose(R.map(R.sortBy(R.prop("dueDate"))), activeByUser);
复制代码
观察力强的读者可能已经注意到咱们能够将上述全部内容合并起来。既然咱们的compose函数容许两个以上的参数,为何不在一个步骤中完成全部这些工做呢?
var sortUserTasks = R.compose(
R.mapObj(R.sortBy(R.prop('dueDate'))),
groupByUser,
R.filter(R.where({complete: false})
);
复制代码
若是您没有其余地方调用函数activebyuser和incomplete,这样写多是合理的。可是,它也会使调试变得更困难,而且不会增长代码的可读性。
事实上,我认为咱们不该该把全部函数合并成一个函数。应该拆分可重用的部分。若是咱们这样作,可能会更好:
var sortByDateDescend = R.compose(R.reverse, sortByDate);
var sortUserTasks = R.compose(R.mapObj(sortByDateDescend), activeByUser);
复制代码
若是咱们肯定咱们只想先按最近的日期排序,那么咱们能够只单独保留SortByDatedDescend函数。若是业务有按升序或降序对数据进行排序两种需求,应该保留sortByDate和sortByDateDescend函数都在,方便后续的组合。
咱们这回尚未处理数据。这是怎么回事?没有数据的数据处理只是过程。耐心写,当您使用函数式编程时,您所获得的只是组成管道的函数。一个函数向下一个函数提供数据,下一个函数向下下个函数提供数据,依此类推,直到须要的结果从末尾流出。
到目前为止,咱们已经构建了如下函数:
incomplete: [Task] -> [Task]
sortByDate: [Task] -> [Task]
sortByDateDescend: [Task] -> [Task]
activeByUser: [Task] -> {String: [Task]}
sortUserTasks: {String: [Task]} -> {String: [Task]}
复制代码
咱们已经使用前面的函数来构建sortUserTasks,也能够单独使用这些函数。这里面的activeByUser函数,其中的groupByUser函数,我尚未实现。咱们要怎样编写它呢?
如下是groupByUser函数的实现:
var groupByUser = R.partition(R.prop('username'));
复制代码
从任务列表中选择前五个元素,咱们可使用ramda的take函数,咱们能够这样作:
var topFiveUserTasks = R.compose(R.mapObj(R.take(5)), sortUserTasks);
复制代码
咱们只须要返回的对象中属性的子集,好比标题和截止日期。在这个数据结构中,用户名显然是多余的,咱们不想传递给其余系统。
咱们可使用Ramda的相似于SQL select函数的方法来实现这一点,该函数被称为project:
var importantFields = R.project(['title', 'dueDate']);
var topDataAllUsers = R.compose(R.mapObj(importantFields), topFiveUserTasks);
复制代码
如今,咱们的todolist应用程序中,可能有下面这些函数:
var incomplete = R.filter(R.where({complete: false}));
var sortByDate = R.sortBy(R.prop('dueDate'));
var sortByDateDescend = R.compose(R.reverse, sortByDate);
var importantFields = R.project(['title', 'dueDate']);
var groupByUser = R.partition(R.prop('username'));
var activeByUser = R.compose(groupByUser, incomplete);
var topDataAllUsers = R.compose(R.mapObj(R.compose(importantFields,
R.take(5), sortByDateDescend)), activeByUser);
复制代码
如今是将数据传递到函数中的时候了。这些函数都接受相同类型的数据,即一个todo项数组。咱们没有具体描述这些项目的结构,但咱们知道它们必须至少具备如下属性:
complete: Boolean
dueDate: String, formatted YYYY-MM-DD
title: String
userName: String
因此,若是咱们有一个任务数组,咱们如何使用它?以下:
var 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 infrastucture'},
{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'}
]
}
复制代码
一样,咱们也能够将任务数组传递给incomplete函数,获得一个筛选后的列表:
var incompleteTasks = incomplete(tasks);
复制代码
结果以下:
[
{
username: 'Scott',
title: 'Add `mapObj`',
dueDate: '2014-06-09',
complete: false,
effort: 'low',
priority: 'medium'
}, {
username: 'Michael',
title: 'Finish algebraic types',
dueDate: '2014-06-15',
complete: false,
effort: 'high',
priority: 'high'
} /*, ... */
]
复制代码
固然,您也能够将任务数组传递给sortbydate、sortbydatedescend、importantfields、byuser或activebyuser。由于这些都在相似的类型上运行——任务数组——咱们能够经过简单的组合构建一个大型的工具集合。
如今又有了一个新需求,咱们的项目又要支持一个新特性,为特定用户筛选任务列表。拥有同上面相同的筛选,排序等功能。
var gloss = R.compose(importantFields, R.take(5), sortByDateDescend);
var topData = R.compose(gloss, incomplete);
var topDataAllUsers = R.compose(R.mapObj(gloss), activeByUser);
var byUser = R.use(R.filter).over(R.propEq("username"));
复制代码
下面是使用方式:
var results = topData(byUser('Scott', tasks));
复制代码
能够,如:
var incomplete = R.filter(R.where({complete: false}));
复制代码
咱们不先获得复合函数,再操做,而是直接获得数据结果:
var incompleteTasks = R.filter(R.where({complete: false}), tasks);
复制代码
全部其余主要函数也是如此:只需在调用结束时添加一个tasks参数,就能够返回数据。
Ramda的一个主要特性。就是全部函数都是自动柯里化的。这意味着,若是您没有提供函数指望的全部参数,将返回一个新的函数,此函数缓存了已经传递的参数,指望剩余的参数。上面的代码中,就是使用了柯里化这一特性,好比R.filter期待两个参数,咱们只传递给它一个,那么它就返回一个新函数,指望再传递给新函数一个参数,才执行获得筛选出的最终数据。
自动柯里化特性,加上Ramda这种函数优先,数据最后的API设计风格,使Ramda很是适合编写函数式编程风格。
node环境使用npm安装:
npm install ramda
var R = require('ramda')
复制代码
浏览器环境:
<script src="path/to/yourCopyOf/ramda.js"></script>
复制代码
或
<script src="path/to/yourCopyOf/ramda.min.js"></script>
复制代码
或使用一些CDN连接。
英文原文地址:fr.umio.us/why-ramda/