当前位置:首页 > 技术 > 正文内容

为什么选择Ramda?

访客 技术 2026年7月28日 1

为什么选择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}))
);

我的回答是,如果没有其他地方需要用到中间函数activeByUserpending,这样做可能是合理的。但这会使调试变得更加困难,并且不会显著提高代码的可读性。

实际上,我认为我们应该反其道而行之。我们内部使用了一个相当复杂的部分,这部分本身可能是可复用的。也许我们应该这样做:

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-DD
  • title: 字符串
  • 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'}
    ]
}
返回列表

上一篇:基于TensorFlow实现中文文本情感分类系统

没有最新的文章了...

相关文章

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

linux screen 用法详情 (nohup 的替代方案)

一、screen 是什么?能干嘛?screen 是一个终端复用器,可以:在一个 SSH 会话中开多个“虚拟终端”SSH 断线后,程序仍然在后台运行随时重新连接到原来的会话特别适合:nohup 的替代方案跑脚本 / 爬虫 / 训练模型运维、远程开发二、安装 screen# CentOS / Rocky / Almayum install -y screen# Debian / Ubuntuapt i...

PHPStan 有什么用?怎么用?

PHPStan 是一个 PHP 的静态分析工具,在不运行代码的情况下就能帮你发现潜在问题,比如:传错类型(把 string 传给接受 int 的函数)访问不存在的属性 / 方法null 没处理好永远不会执行到的代码数组 key/值类型不一致返回值不符合声明注释和真实类型不匹配它非常适合:想提升代码质量、减少线上 bug、统一团队风格的人(尤其是中大型项目)。一、PHPStan 有什么用(通俗点说)...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。