首页 > web前端 > js教程 > 与ramda.js的实践功能编程

与ramda.js的实践功能编程

William Shakespeare
发布: 2025-02-17 10:40:10
原创
257 人浏览过

Hands-on Functional Programming with Ramda.js

本文经 Yaphi Berhanu、Vildan Softic、Jani Hartikainen 和 Dan Prince 审核。感谢所有 SitePoint 的同行评审员,使 SitePoint 内容达到最佳状态!

JavaScript 的魅力之一在于其函数式编程特性。从一开始,函数就是 JavaScript 世界中的一等公民。这使得编写优雅且富有表现力的代码成为可能,这些代码可以以多种方式轻松组合在一起。然而,仅仅具备进行函数式编程的能力并不能自动实现函数式编程。Ramda.js 是一个非常流行的库(在 GitHub 上拥有超过 4k 星),我们可以使用它来帮助我们开始使用 JavaScript 进行函数式编程。

要点

  • Ramda.js 通过确保函数不改变其输入数据来增强 JavaScript 的函数式编程能力,从而提高可预测性和易于测试性。
  • Ramda.js 的主要特性包括函数的自动柯里化和对不变性的强烈强调,这使得更容易构建没有副作用的函数式管道。
  • 该库适用于前端和 Node.js 环境,可以与其他 JavaScript 库或框架无缝集成。
  • Ramda.js 提供了诸如 R.mapR.propR.curry 之类的实用方法和函数,简化了数据结构的操作,并增强了代码的可读性和可维护性。
  • 尽管 Ramda.js 具有诸多优点,但它并非万能的解决方案;开发人员在决定是否使用它时,应该考虑具体的项目需求和团队对函数式编程的熟悉程度。

入门

为了充分利用 Ramda.js,我们应该通过创建一个小型 Node.js 项目来熟悉它的优点。我们可以通过 Node 包管理器 (npm) 简单地安装它。

npm install ramda
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

通常,我们将简单地将库的功能导入到命名空间 R 中。这样,对 Ramda 方法的所有调用都将具有 R. 前缀。

var R = require('ramda');
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

当然,没有什么可以阻止我们在前端代码中使用 Ramda.js。在浏览器中,我们只需要包含指向库副本的正确路径即可。这可能与以下 HTML 代码片段一样简单。

<🎜>
登录后复制
登录后复制
登录后复制
登录后复制

Ramda.js 不使用任何 DOM 或 Node.js 特定的特性。它只是一个语言库/扩展,并且建立在 JavaScript 运行时(如 ECMAScript 5 中标准化的那样)已经公开的结构和算法之上。准备深入研究吗?让我们看看一些功能的实际应用!

概念

函数式编程中最重要的概念是纯函数的概念。纯函数是幂等的,不会改变任何状态。从数学角度来看,这很有意义,因为像 sin(x) 这样的函数似乎非常自然,并且不依赖于任何外部状态。除了纯函数之外,我们还希望拥有单参数函数。它们是最原始的。零参数函数通常表示将更改外部状态,因此不是纯函数。但在像 JavaScript 这样的语言中,我们通常会有多个参数的函数。

柯里化

具有高阶函数(即可以将函数作为输入并发出函数作为输出的函数)的能力与闭包(捕获局部变量)相结合,为我们提供了一种很好的方法:柯里化。柯里化是一个过程,其中一个具有多个(假设为 n 个)参数的函数被转换为一个具有单个参数的函数,该函数返回另一个具有单个参数的函数。这将持续进行,直到收集所有必需的参数。假设我们想使用 Ramda.js 辅助函数来编写一个单参数包装器,该包装器测试其参数是否为字符串。以下代码将完成这项工作。

npm install ramda
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

使用柯里化可以更容易地做到这一点。由于 R.is 是 Ramda.js 的一部分,因此如果我们提供的参数少于函数所需要的参数,该库将自动返回一个柯里化函数:

var R = require('ramda');
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

这更具表现力。由于我们使用单个参数调用了 R.is,因此我们得到了一个函数。在第二次调用(记住,原始函数调用需要两个参数)中,我们获得了结果。但是,如果我们一开始没有使用 Ramda.js 的辅助函数呢?让我们假设我们已经在代码中的某个地方定义了以下函数:

<🎜>
登录后复制
登录后复制
登录后复制
登录后复制

这是完整的二阶多项式。它有四个参数,允许所有可能的值。但是通常,我们只想为一组固定的参数 a、b 和 c 更改 x。让我们看看如何使用 Ramda.js 来转换它:

function isString (test) {
    return R.is(String, test);
}

var result = isString('foo'); //=> true
登录后复制
登录后复制
登录后复制

同样,我们能够简单地使用参数求值来为特定子集创建别名。例如,方程 x - 1 可以通过以下方式获得:

var isString = R.is(String);
var result = isString('foo'); //=> true
登录后复制
登录后复制
登录后复制

在参数数量不是由我们函数的参数给定的情况下,我们需要使用 curryN 并显式指定参数数量。柯里化是 Ramda.js 的核心,但如果没有更多内容,该库似乎就不那么有趣了。函数式编程中另一个重要的概念是不变性。

不变结构

防止函数更改状态的最简单方法是只使用不能更改的数据结构。对于简单的对象,我们需要只读访问器,例如:

var quadratic = (a, b, c, x) => x * x * a + x * b + c;
quadratic(1, 0, 0, 2); //=> 4
quadratic(1, 0, 0)(2); //=> TypeError: quadratic(..) is not a function
登录后复制
登录后复制
登录后复制

是不允许的。除了声明只读属性外,我们还可以将它们转换为 getter 函数:

var quadratic = R.curry((a, b, c, x) => x * x * a + x * b + c);
quadratic(1, 0, 0, 2); //=> 4
quadratic(1, 0, 0)(2); //=> 4
登录后复制
登录后复制

现在这已经好多了,但是,对象仍然可以更改。这意味着有人可以添加 getX 函数的自定义定义,例如:

npm install ramda
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

实现不变性的最佳方法是使用 Object.freeze。结合 const 关键字,我们可以引入一个不能更改的不变变量。

var R = require('ramda');
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

另一个例子涉及列表。将元素添加到不可变列表需要复制原始列表并将新元素添加到末尾。当然,我们也可以使用原始对象的不变性知识来优化实现。这样,我们可以用简单的引用替换副本。本质上,这可能会变成一种链表。我们应该意识到,标准 JavaScript 数组是可变的,因此需要复制以确保正确性。诸如 append() 之类的方法在 JavaScript 数组上工作并返回此类数组。该操作是幂等的;如果我们多次使用相同的参数调用该函数,我们将始终获得相同的结果。

<🎜>
登录后复制
登录后复制
登录后复制
登录后复制

还有一个 remove 方法,它返回给定数组,但不包含指定的条目。它的工作原理如下:

function isString (test) {
    return R.is(String, test);
}

var result = isString('foo'); //=> true
登录后复制
登录后复制
登录后复制

由于它具有灵活的参数数量,因此我们需要前面提到的 curryN 函数来应用柯里化。还有一些有用的通用辅助函数可用。

实用方法

所有辅助函数最重要的概念是参数是有序的以方便柯里化。参数越频繁地被更改,它就越不可能位于其他参数之前。

sum() 和 range()

Ramda.js 中当然可以找到通常的函数,例如 sum 和 range:

var isString = R.is(String);
var result = isString('foo'); //=> true
登录后复制
登录后复制
登录后复制

对于 range() 辅助函数,我们可以使用柯里化创建一个包装器:

var quadratic = (a, b, c, x) => x * x * a + x * b + c;
quadratic(1, 0, 0, 2); //=> 4
quadratic(1, 0, 0)(2); //=> TypeError: quadratic(..) is not a function
登录后复制
登录后复制
登录后复制

如果我们想用固定的(独占)最大值来包装它呢?Ramda.js 通过使用由 R.__ 表示的特殊参数来满足我们的需求:

var quadratic = R.curry((a, b, c, x) => x * x * a + x * b + c);
quadratic(1, 0, 0, 2); //=> 4
quadratic(1, 0, 0)(2); //=> 4
登录后复制
登录后复制

map()

此外,Ramda.js 尝试使用“更好”的解决方案来提供 JavaScript 核心函数(例如 Array.prototype.map)的替代方案。“更好”的解决方案具有不同的参数顺序和开箱即用的柯里化。对于 map 函数,如下所示:

var xOffset = quadratic(0, 1, -1);
xOffset(0); //=> -1
xOffset(1); //=> 0
登录后复制

prop()

另一个有用的实用程序是 prop 函数,它尝试获取指定属性的值。如果给定的属性不存在,则返回 undefined。如果值确实是 undefined,这可能会模棱两可,但在实践中我们很少会关心。

var position = {
    x: 5,
    y: 9
};
position.x = 10; // works!
登录后复制

zipWith()

如果前面介绍的方法没有让你相信 Ramda.js 可能提供一些有用的东西,那么接下来的方法可能更有趣。这次我们不会讨论一个具体的例子,而是看看任意选择的场景。假设我们有两个列表,我们想将它们连接起来。使用 zip 函数实际上非常简单。但是,通常的结果(元素数组,它们本身是双值数组)可能不是我们想要的结果。这就是 zipWith 函数发挥作用的地方。它使用任意函数将值映射到单个值。

var position = (function (x, y) {
    return {
        getX: () => { return x; },
        getY: () => { return y; }
    };
})(5, 9);
position.getX() = 10; // does not work!
登录后复制

类似地,我们可以为向量引入点积:

npm install ramda
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

我们通过乘法压缩两个数组(产生 [1, 4, 9]),并将结果传递给 sum 函数。无论如何,使用可枚举对象是重要的主题。Ramda.js 提供了许多有用的辅助函数也就不足为奇了。我们已经介绍了 R.map 来将函数应用于每个元素。类似地,有一些辅助函数可以减少元素的数量。可以通过最通用的 filter 函数(产生另一个数组)或通过 reduce 函数产生单个值。

chain()

对数组的操作带有一些有用的辅助函数。例如,使用 chain,我们可以轻松合并数组。假设我们有一个函数 primeFactorization,它使用数字作为输入并给出带有素数因子的数组作为输出,我们可以将应用该函数的结果与一组数字组合如下:

var R = require('ramda');
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

一个实际的例子

到目前为止,一切都很顺利。现在最大的问题是:通过使用 Ramda.js 引入的这些概念,我们在日常工作中有哪些好处?让我们假设我们有以下(已经非常好看的)代码片段。

<🎜>
登录后复制
登录后复制
登录后复制
登录后复制

如何使用 Ramda.js 使其更具可读性?好吧,第一行已经足够好了。第二行已经很混乱了。我们真正想要的是提取提供的参数的 posts 属性。最后,我们有一个有点混乱的第三行。在这里,我们尝试迭代所有帖子(由参数提供)。同样,它的唯一目的是提取特定属性。以下解决方案如何?

function isString (test) {
    return R.is(String, test);
}

var result = isString('foo'); //=> true
登录后复制
登录后复制
登录后复制

由于 Ramda.js 赋能的函数式编程,这可能是关于可读性的最佳解决方案。但是,我们应该注意,ECMAScript 6 中引入的“胖箭头”语法也导致了非常简洁、易读的代码:

var isString = R.is(String);
var result = isString('foo'); //=> true
登录后复制
登录后复制
登录后复制

这几乎同样易于阅读,无需任何 Ramda.js 知识。此外,我们减少了抽象的数量——这只会对性能和可维护性有利。

透镜

最后,我们还应该讨论有用的对象辅助函数。这里值得一提的是 lens 函数。透镜是一个特殊的对象,可以与对象或数组一起传递给某些 Ramda.js 函数。它允许这些函数分别从对象或数组的特定属性或索引检索或转换数据。假设我们有一个具有两个键 x 和 y 的对象——就像文章开头给出的不变性示例一样。与其将对象包装在另一个具有 getter 和 setter 方法的对象中,我们可以创建一个透镜来“关注”感兴趣的属性。要创建一个访问对象的 x 属性的透镜,我们可以执行以下操作:

var quadratic = (a, b, c, x) => x * x * a + x * b + c;
quadratic(1, 0, 0, 2); //=> 4
quadratic(1, 0, 0)(2); //=> TypeError: quadratic(..) is not a function
登录后复制
登录后复制
登录后复制

虽然 prop 是一个标准的 getter(这已经介绍过了),但 assoc 是一个 setter 函数(三个值语法:键、值、对象)。现在我们可以使用 Ramda.js 中的函数来访问此透镜定义的属性。

npm install ramda
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

请注意,该操作不会触及给定的 position 对象(无论我们是否冻结它)。应该注意的是,set 只是 over 的一个特例,它类似但采用函数而不是任意值。然后将使用该函数来转换值。例如,以下调用将 x 坐标乘以 3:

var R = require('ramda');
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

Ramda.js、lodash 或其他?

一个合理的问题当然是为什么选择 Ramda.js——为什么我们不使用 lodash 或其他东西呢?当然,有人可能会争辩说 Ramda.js 更新,因此必须更好,但这与事实相去甚远。事实是,Ramda.js 是考虑到函数式原则而构建的——在参数放置和选择方面采用了新的方法(对于 JavaScript 库)。例如,Ramda.js 中的列表迭代器默认只传递项目,而不是列表。另一方面,其他库(如 lodash)的标准是将项目和索引传递给回调函数。这似乎是一个细微的问题,但它阻止你使用方便的内置函数,如 parseInt()(它带有一个可选的第二个参数),而使用 Ramda.js,这可以很好地工作。最终,选择什么取决于具体的要求或团队的经验和/或知识,但肯定有一些很好的理由让 Ramda.js 受到应有的关注。

进一步阅读

  • 高阶函数
  • 为什么柯里化有帮助
  • 不变性
  • 为什么选择 Ramda?
  • Ramda 文档
  • 使用 Ramda.js 进行函数式编程

结论

函数式编程不应被视为灵丹妙药。相反,它应该被视为我们现有工具箱的一个自然补充,它为我们提供了更高的可组合性、更大的灵活性和更高的容错性/鲁棒性。现代 JavaScript 库已经尝试采用一些函数式概念来利用这些优势。Ramda.js 是一个强大的工具,可以扩展你自己的函数式实用程序库。你对函数式编程的看法是什么?你认为它在哪些方面表现出色?请在评论中告诉我!

关于使用 Ramda 进行函数式编程的常见问题解答 (FAQs)

使用 Ramda 进行函数式编程的主要优势是什么?

Ramda 是一个针对 JavaScript 程序员的实用函数式库。它专为函数式编程风格而设计,这使得创建函数式管道更容易,并且永远不会改变用户数据。使用 Ramda 的主要优势在于它强调不变性和无副作用函数。这意味着函数不会改变其输入数据,使你的代码更易于预测和测试。Ramda 的函数是自动柯里化的,这使你可以轻松地从旧函数构建新函数,而不会迷失在一堆括号或回调地狱中。

Ramda 与其他 JavaScript 库有何不同?

与其他 JavaScript 库不同,Ramda 旨在支持函数式编程并且不会改变数据。它提供了一组默认情况下是柯里化的实用函数,这意味着它们旨在一起使用以创建新函数。这与 Underscore 或 Lodash 等库有很大不同,这些库默认情况下不是柯里化的,并且通常需要你编写额外的代码才能获得相同的结果。Ramda 的 API 也更一致且更易于使用,重点是简单性和可读性。

我可以将 Ramda 与其他 JavaScript 库或框架一起使用吗?

是的,Ramda 可以与其他 JavaScript 库和框架一起使用。它是一个独立的库,不依赖于任何其他库或框架,并且不会修改 JavaScript 对象原型。这使得它可以安全地与其他库或框架一起使用,而无需担心冲突或意外副作用。无论你使用的是 jQuery、React、Angular、Vue 还是任何其他库或框架,你都可以使用 Ramda 来帮助编写更简洁、更函数式的代码。

Ramda 适用于函数式编程初学者吗?

Ramda 是函数式编程新手的一个很棒的工具。它的 API 设计简单直观,命名约定清晰一致。文档也非常详尽,有很多示例可以帮助你入门。但是,像任何新的工具或范例一样,都存在学习曲线。可能需要一些时间才能习惯函数式思维,并理解如何有效地使用 Ramda 的函数。但是,随着练习,你会发现它可以极大地简化你的代码,并使代码更容易理解。

Ramda 如何处理 null 和 undefined 值?

Ramda 将 null 和 undefined 值视为空值,类似于其他函数式编程语言如何处理它们。这意味着你可以安全地将 null 或 undefined 传递给 Ramda 的函数,而不会导致错误或异常。但是,在将 null 或 undefined 值传递给函数之前,始终检查它们是一个好主意,以避免意外行为。

我可以在 Node.js 环境中使用 Ramda 吗?

是的,Ramda 可用于 Node.js 环境。它是一个通用的库,可在浏览器和 Node.js 中使用。你可以通过 npm 安装它,并在你的 Node.js 模块中像其他任何包一样需要它。

Ramda 如何处理异步操作?

Ramda 没有内置的异步操作支持,因为它主要是一个同步库。但是,你可以将其与支持异步操作的其他库(如 Promises 或 async/await)一起使用。你也可以在异步函数或 then 回调中使用 Ramda 的函数。

如何为 Ramda 项目做出贡献?

Ramda 是一个开源项目,欢迎随时贡献。你可以通过报告错误、建议新功能、改进文档或提交拉取请求来做出贡献。在贡献之前,最好阅读 Ramda GitHub 页面上的贡献指南。

Ramda 是否仍在维护和更新?

是的,Ramda 正在积极维护并定期更新。维护人员致力于使库保持最新状态并解决出现的任何问题或错误。你可以查看 GitHub 页面以获取最新的更新和版本。

我可以将 Ramda 用于商业项目吗?

是的,Ramda 采用 MIT 许可证授权,这意味着你可以将其用于商业项目。但是,始终阅读完整的许可协议以了解你的权利和责任是一个好主意。

以上是与ramda.js的实践功能编程的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板