阅读 236

【译】为什么使用 Ramda

对Ramda的两种态度

当Buzzdecafe(Ramda库的主要贡献者)最近将Ramda介绍给世界时,有两种截然不同的反应。那些习惯了函数式技术的人——不论是用JavaScript或其他语言——大多会回答“酷”。他们可能对此感到兴奋,或者只是随便地注意到另一个潜在的工具,但他们理解它的用途。

第二种反应是:“哈?”

对于那些不习惯于函数式编程的人来说,ramda似乎毫无用处。它的大部分主要功能已经被诸如Underscore和lodash之类的库所实现了。

这些人是对的。如果你想用你一直使用的命令式和面向对象风格来编写代码,Ramda没有更多的功能提供给你。

然而,它确实提供了一种不同的编码风格,这种风格在纯函数式编程语言中被认为是理所当然的:Ramda通过函数组合的方式使构建复杂逻辑变得简单。请注意,任何具有compose函数的库都允许您进行函数组合;但Ramda真正的要点是:“使其简单化”。

Ramda是如何运作

让我们看看Ramda是如何运作的。

Web框架总是拿“todolist”当做事例,因此我们也用它做事例:想象这样一个场景,筛选todolist以删除所有已完成的项。

使用内置的数组原型方法,我们可以这样做:

// 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还有更多方法

从任务列表中选择前五个元素,我们可以使用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非常适合编写函数式编程风格。

使用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/

关注下面的标签,发现更多相似文章
评论