ramda
本文由Yaphi Berhanu , Vildan Softic , Jani Hartikainen和Dan Prince进行了同行评审。 感谢所有SitePoint的同行评审员使SitePoint内容达到最佳状态!
对我来说,使JavaScript如此有趣的原因之一是该语言的功能方面。 从一开始,函数就已经成为JavaScript世界中的一等公民。 这样就可以编写优雅而富有表现力的代码,并且可以通过多种方式轻松地将它们组合在一起。
但是,仅仅具有执行某些功能性编程的能力并不能自动神奇地导致功能性编程。 Ramda.js是一个非常受欢迎的库(在GitHub上有4k颗星),我们可以使用它来帮助我们开始使用JavaScript进行函数式编程。
入门
要完全使用Ramda.js,我们应该通过创建一个小的Node.js项目来适应它的好处。 我们可以简单地通过Node Package Manager (npm)安装它。
npm install ramda
通常,我们只是将库的功能导入名称空间R
这样,所有对Ramda方法的调用都将具有R.
前缀。
var R = require('ramda');
当然,没有什么可以阻止我们在前端代码中使用Ramda.js。 在浏览器中,我们只需要包含指向库副本的正确路径。 这可能与以下HTML代码段一样简单。
<script src="ramda.min.js"></script>
Ramda.js不使用任何DOM或Node.js特定功能。 它只是一个语言库/扩展,并且建立在JavaScript运行时已经公开的结构和算法的基础上(在ECMAScript 5中已标准化)。
准备潜水了吗? 让我们看看一些实际的能力!
概念
函数式编程中最重要的概念是纯函数的概念 。 纯函数是幂等的 ,不会更改任何状态。 从数学上讲,这是有道理的,因为诸如sin(x)
函数似乎很自然,并且不依赖任何外部状态。
除了具有纯函数之外,我们还希望具有单个参数函数。 它们是最原始的。 零自变量函数通常指示外部状态将被更改,因此不是纯状态。 但是在像JavaScript这样的语言中,我们通常会使用带有多个参数的函数。
咖喱
具有闭包(捕获局部变量)的高阶函数(即可以将函数作为输入并发出函数作为输出的函数)的能力为我们提供了一种不错的解决方法: currying 。 咖喱化是一个过程,其中将具有多个(例如n
)参数的函数转换为具有单个参数的函数,并返回具有单个参数的另一个函数。 一直进行到收集所有必需的参数为止。
假设我们要使用Ramda.js帮助程序is
编写一个单参数包装器,以测试其参数是否为string
。 以下代码将完成此工作。
function isString (test) {
return R.is(String, test);
}
var result = isString('foo'); //=> true
使用curring可以轻松完成同一件事。 由于R.is
是R.is
的一部分,因此,如果我们提供的参数较少,则该库将自动返回curried函数:
var isString = R.is(String);
var result = isString('foo'); //=> true
这更具表现力。 由于我们将R.is
与单个参数一起使用, R.is
我们收到了一个函数。 在第二次调用(记住,原始函数调用需要两个参数)时,我们获得了结果。
但是,如果我们首先没有从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
这是完整的二阶多项式。 它有四个参数,允许所有可能的值。 但是通常,我们只想更改固定参数a
, b
和c
的x
。 让我们看看如何使用Ramda.js进行转换:
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
同样,我们能够简单地使用参数求值来别名特定的子集。 例如,方程x - 1
可以通过以下公式获得:
var xOffset = quadratic(0, 1, -1);
xOffset(0); //=> -1
xOffset(1); //=> 0
如果函数的参数未提供参数数量,则需要使用curryN
并显式指定参数数量。
咖喱是Ramda.js的核心,但如果没有其他功能,该库似乎就没那么有趣了。 在函数式编程中另一个重要的概念是不变性。
不变的结构
![](https://img-blog.csdnimg.cn/2022010617213987700.png)
免费学习PHP!
全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。
原价$ 11.95 您的完全免费
防止功能更改状态的最简单方法是仅对无法更改的数据结构进行操作。 然后,对于简单对象,我们需要只读访问器,例如
var position = {
x: 5,
y: 9
};
position.x = 10; // works!
将不会被允许。 除了将属性声明为只读之外,我们还可以将其变为getter函数:
var position = (function (x, y) {
return {
getX: () => { return x; },
getY: () => { return y; }
};
})(5, 9);
position.getX() = 10; // does not work!
现在,这已经好了一点,但是仍然可以更改对象。 这意味着某人可以只添加getX
函数的自定义定义,例如:
position.getX = function () {
return 10;
};
实现不变性的最佳方法是使用Object.freeze
。 与const
关键字一起,我们可以引入一个不可更改的不变变量。
const position = Object.freeze({ x: 5, y: 9 });
另一个示例将涉及列表。 然后,将元素添加到不可变列表中需要您复制原始列表,并将新元素添加到末尾。 当然,我们也可以使用原始对象的不变性知识来优化实现。 这样,我们可以用简单的参考替换副本。 本质上,这可能会成为一种链表。 我们应该意识到,标准JavaScript数组是可变的,因此需要复制以确保正确性。
诸如append()
方法可用于JavaScript数组,并返回此类数组。 该操作是幂等的; 如果我们使用相同的参数多次调用该函数,我们将始终获得相同的结果。
R.append('tests', ['write', 'more']); //=> ['write', 'more', 'tests']
R.append('tests', ['write', 'more']); //=> ['write', 'more', 'tests']
R.append('tests', ['write', 'more']); //=> ['write', 'more', 'tests']
还有一个remove
方法可返回不包含指定条目的给定数组。 其工作方式如下:
R.remove('write', 'tests', ['write', 'more', 'tests']); //=> ['more']
由于它具有大量的参数,因此我们需要前面提到的curryN
函数来应用currying。 也有一组有用的常规助手。
实用方法
所有助手功能最重要的概念是对参数进行排序以方便进行计数。 一个参数应被更改得越频繁,它被置于其他某个参数之前的可能性就越小。
sum()和range()
当然,通常可以在Ramda.js中找到诸如sum和range之类的可疑对象:
R.sum(R.range(1, 5)); //=> 10
因此,对于range()
助手,我们可以使用currying创建包装器:
var from10ToExclusive = R.range(10);
from10ToExclusive(15); //=> [10, 11, 12, 13, 14]
如果我们想用固定的(最大)最大值来包装该怎么办? 值? Ramda.js通过使用R.__
表示的特殊参数来覆盖我们:
var to14FromInclusive = R.range(R.__, 15);
to14FromInclusive(10); //=> [10, 11, 12, 13, 14]
地图()
此外,Ramda.js尝试通过“更好”的解决方案提供JavaScript核心功能的替代方案,例如Array.prototype.map
。 这些替代方案具有不同的参数顺序和即席即用的curring。
对于map函数,其外观如下:
R.map(x => 2 * x, [1, 2, 3]); //=> [2, 4, 6]
Struts()
另一个有用的实用程序是prop函数,它试图获取指定属性的值。 如果给定的属性不存在,则返回undefined
。 如果确实undefined
该值,则可能会模棱两可,但实际上我们很少关心。
R.prop('x', { x: 100 }); //=> 100
R.prop('x', { y: 50 }); //=> undefined
zipWith()
如果先前介绍的方法不能使您相信Ramda.js可能提供了有用的东西,那么接下来的这些可能会更有趣。 这次,我们将不讨论特定的示例,而是查看任意选择的方案。
假设我们有两个列表,我们想加入它们。 使用zip
函数实际上非常简单。 但是,通常的结果(元素数组本身就是二值数组)可能不是期望的结果。 这是zipWith函数起作用的地方。 它使用任意函数将值映射到单个值。
var letters = ["A", "B", "C", "D", "E"];
var numbers = [1, 2, 3];
var zipper = R.zipWith((x, y) => x + y);
zipper(letters, numbers); // ["A1", "B2", "C3"]
同样,我们可以为向量引入点积:
var dot = R.pipe(R.zipWith((x, y) => x * y), R.sum);
dot([1, 2, 3], [1, 2, 3]) // 14
我们通过乘法压缩两个数组(产生[1, 4, 9]
),然后将结果传递给sum函数。
无论如何,与枚举对象合作是一个大主题。 Ramda.js带来了很多有用的帮助,这并不奇怪。 我们已经引入了R.map
来对每个元素应用一个函数。 同样,有一些帮助程序可以减少元素数量。 通过最通用的filter
功能(产生另一个数组)或通过reduce
函数将其设置为单个值。
链()
在数组上进行操作附带了一些有用的辅助功能。 例如,使用链,我们可以轻松合并数组。 假设我们有一个函数primeFactorization
使用数字作为输入,并给出一个带有素数的数组作为输出,我们可以将应用该函数的结果与一组数字结合起来,如下所示:
R.chain(primeFactorization, [4, 7, 21]); //=> [2, 2, 7, 3, 7]
一个实际的例子
到目前为止,一切都很好。 现在最大的问题是:通过使用Ramda.js引入的这些概念,我们在日常工作中有什么好处? 假设我们有以下(已经很好看)的代码片段。
fetchFromServer()
.then(JSON.parse)
.then(function (data){ return data.posts })
.then(function (posts){
return posts.map(function (post){ return post.title })
});
如何使用Ramda.js使其更具可读性? 好吧,第一行就可以了。 第二个已经很混乱。 我们真正想要的只是提取所提供参数的posts
属性。
最后,我们有一类凌乱的第三行。 在这里,我们尝试遍历所有帖子(由参数提供)。 同样,提取特定属性的唯一目的。 下列解决方案如何:
fetchFromServer()
.then(JSON.parse)
.then(R.prop('posts'))
.then(R.map(R.prop('title')));
得益于Ramda.js所支持的功能性编程,这可能与关于可读性的最佳解决方案接近。 但是,我们应该注意,ECMAScript 6中引入的“胖箭头”语法也导致了非常简洁易读的代码:
fetchFromServer()
.then(JSON.parse)
.then(json => json.posts)
.then(posts => posts.map(p => p.title));
这几乎是可读的,不需要任何Ramda.js知识。 此外,我们减少了抽象的数量–仅对性能和可维护性有益。
镜片
最后,我们还应该讨论有用的对象助手。 这里的镜头功能值得一提。
镜头是一个特殊的对象,可以与对象或数组一起传递给某些Ramda.js函数。 它允许那些函数分别从对象或数组的特定属性或索引中检索或转换数据。
假设我们有一个带有两个键x
和y
的对象–就像本文开头给出的不变性示例一样。 无需使用getter和setter方法将对象包装在另一个对象中,我们可以创建一个镜头来“关注”感兴趣的属性。
要创建访问对象的属性x
的镜头,我们可以执行以下操作:
var x = R.lens(R.prop('x'), R.assoc('x'));
prop
是标准的获取方法(已经介绍过),而assoc是setter函数(三个值的语法:键,值,对象)。
现在,我们可以使用Ramda.js中的函数来访问此镜头定义的属性。
var xCoordinate = R.view(x, position);
var newPosition = R.set(x, 7, position);
请注意,该操作使给定的position
对象保持不变(与是否冻结它无关)。
应该注意的是, set只是over的特化 ,这是相似的,但是采用函数而不是任意值。 然后,该函数将用于转换值。 例如,以下调用将x坐标乘以3:
var newPosition = R.over(x, R.multiply(3), position);
Ramda.js,lodash还是其他?
一个合理的问题肯定是为什么选择Ramda.js –为什么我们不应该使用lodash或其他任何方法呢? 当然,有人可以说Ramda.js是较新的,因此必须更好,但事实并非如此。 事实是,Ramda.js是在考虑功能原理的基础上构建的-针对参数放置和选择采用了新的方法(对于JavaScript库)。
例如,Ramda.js中的列表迭代器默认仅传递项目,而不传递列表。 另一方面,其他库(如lodash)的标准是将项目和索引传递给回调函数。 这似乎是一个微妙的问题,但是它使您无法使用方便的内置函数,例如parseInt()
(需要一个可选的第二个参数),而使用Ramda.js则效果很好。
最后,选择什么的决定可能取决于特定的要求或团队的经验和/或知识,但是肯定有一些很好的论据来赋予Ramda.js应有的关注。
进一步阅读
结论
函数式编程不应被视为魔术。 相反,它应该被视为对我们现有工具箱的自然补充,可以为我们提供更高的可组合性,更大的灵活性以及更大的容错性/鲁棒性。 现代JavaScript库已经尝试使用一些功能概念来利用这些优势。 Ramda.js是一个功能强大的工具,可通过实用工具扩展您自己的曲目。
您对函数式编程有何看法? 您在哪里看到它的光芒? 在评论中让我知道!
翻译自: https://www.sitepoint.com/functional-programming-with-ramda/
ramda