Ramda风格思维:不变性与对象

1.第一步
2.结合功能
3.部分使用(咖喱)
4.声明式编程
5.典型符号
6.不变性与对象
7.不变性和数组
8.镜片
9.结论


这篇文章是关于函数式编程的一系列文章的第六部分,该文章称为Ramda样式思维。


第五部分中,我们讨论了以无意义表示法编写函数的方法,其中未明确指定带有函数数据的主参数。


那时,我们无法以一种毫无意义的方式重写所有函数,因为我们没有为此所需的某些工具。 现在该学习它们了。


读取对象属性


让我们再次看一下在第五部分中研究的具有投票权的人的定义示例:


const wasBornInCountry = person => person.birthCountry === OUR_COUNTRY const wasNaturalized = person => Boolean(person.naturalizationDate) const isOver18 = person => person.age >= 18 const isCitizen = either(wasBornInCountry, wasNaturalized) const isEligibleToVote = both(isOver18, isCitizen) 

如您所见,我们使isCitizenisEligibleToVote ,但是我们不能使用前三个功能来做到这一点。


正如我们在第四部分中学到的,我们可以通过使用equalsgte使我们的函数更具声明性。 让我们从这个开始:


 const wasBornInCountry = person => equals(person.birthCountry, OUR_COUNTRY) const wasNaturalized = person => Boolean(person.naturalizationDate) const isOver18 = person => gte(person.age, 18) 

为了使这些函数毫无意义,我们需要一种构造函数的方法,以便在表达式的末尾应用person变量。 问题在于,我们需要访问人的属性,现在我们知道执行此操作的唯一方法-这势在必行。


道具


幸运的是,Ramda再次帮助了我们。 它提供了一个prop函数来访问对象的属性。


使用prop ,我们可以将prop('birthCountry', person)重写为prop('birthCountry', person) 。 让我们做吧:


 const wasBornInCountry = person => equals(prop('birthCountry', person), OUR_COUNTRY) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(prop('age', person), 18) 

哇,现在看起来好多了。 但是,让我们继续进行重构。 让我们更改传递给equals的参数的顺序,以使prop在最后。 equals以完全相反的方式工作,因此我们不会破坏任何东西:


 const wasBornInCountry = person => equals(OUR_COUNTRY, prop('birthCountry', person)) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(prop('age', person), 18) 

接下来,让我们使用currying, equalsgte的自然属性,以便创建新的函数, prop调用的结果将应用于这些函数:


 const wasBornInCountry = person => equals(OUR_COUNTRY)(prop('birthCountry', person)) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(__, 18)(prop('age', person)) 

它看起来仍然是最糟糕的选择,但让我们继续。 让我们为所有prop调用再次使用prop


 const wasBornInCountry = person => equals(OUR_COUNTRY)(prop('birthCountry')(person)) const wasNaturalized = person => Boolean(prop('naturalizationDate')(person)) const isOver18 = person => gte(__, 18)(prop('age')(person)) 

再次,不知何故。 但是现在我们看到了一种熟悉的模式。 我们所有的函数都具有相同的图像f(g(person)) ,从第二部分我们知道,这等效于compose(f, g)(person)


让我们将此优势应用到我们的代码中:


 const wasBornInCountry = person => compose(equals(OUR_COUNTRY), prop('birthCountry'))(person) const wasNaturalized = person => compose(Boolean, prop('naturalizationDate'))(person) const isOver18 = person => compose(gte(__, 18), prop('age'))(person) 

现在我们得到了一些东西。 我们所有的函数看起来像person => f(person) 。 从第五部分我们已经知道我们可以使这些功能毫无意义。


 const wasBornInCountry = compose(equals(OUR_COUNTRY), prop('birthCountry')) const wasNaturalized = compose(Boolean, prop('naturalizationDate')) const isOver18 = compose(gte(__, 18), prop('age')) 

开始时,我们的方法做两件事并不明显。 他们转向对象的属性,并用其值准备了一些操作。 这种重构为毫无意义的样式使得这一点非常明确。


让我们看一下Ramda提供的用于处理对象的其他一些工具。


挑选


prop读取对象的一个​​属性并返回其值的情况下, pick从该对象读取许多属性,并仅与它们一起返回一个新对象。


例如,如果我们只需要人员的姓名和年份,则可以使用pick(['name','age'], person)



如果我们只是想知道我们的对象具有一个属性,而不读取其值,则可以使用has函数检查其属性,以及使用hasIn检查原型链: has('name', person)


路径


prop对象属性的地方, 路径更深入嵌套对象。 例如,我们想从更深的结构中提取邮政编码: path(['address','zipCode'], person)


请注意, pathprop更宽容。 如果路径中的任何内容(包括原始参数)为nullundefined ,则path将返回undefined ,而prop在这种情况下将导致错误。


属性/路径或


propOrpathOr类似于proppath结合了defaultTo 。 它们使您能够为在所研究的对象中找不到的属性或路径指定默认值。


例如,当我们不知道人的名字时,我们可以提供一个占位符: propOr('<Unnamed>, 'name', person) 。 注意与prop不同的是,如果personnullundefinedpropOr不会导致错误; 相反,它将返回默认值。


键/值


返回一个包含对象所有已知属性的所有名称的数组。 将返回这些属性的值。 将这些功能与集合的迭代功能结合使用时,这些功能会很有用,我们在第一部分中已经了解了这些功能。


添加,更新和删除属性


现在,我们有许多用于以声明式读取对象的工具,但是进行更改又如何呢?


由于不变性对我们很重要,因此我们不想直接修改对象。 相反,我们要返回以所需方式更改的新对象。


Ramda再次为我们提供了许多好处。


assoc / assocPath


当我们以命令式风格进行编程时,我们可以通过赋值运算符设置或更改人员的姓名: person.name = 'New name'


在功能不变的世界中,我们可以改用assocconst updatedPerson = assoc('name', 'newName', person)


assoc返回具有添加或更新的属性值的新对象,而原始对象保持不变。


我们还可以使用assocPath来更新附加属性: const updatedPerson = assocPath(['address', 'zipCode'], '97504', person)


dissoc / dissocPath /省略


删除属性呢? 势在必行,我们可能想说一下delete person.age 。 在Ramda中 ,我们将使用dissoc :`const UpdatedPerson = dissoc('age',person)


dissocPath大致相同,但适用于更深的对象结构: dissocPath(['address', 'zipCode'], person)


我们还有omit ,它可以一次删除多个属性: const updatedPerson = omit(['age', 'birthCountry'], person)


请注意, pickomit有些相似,并且可以很好地互补。 它们对于白名单(使用pick只保存某些属性集)和黑名单(通过使用omit摆脱某些属性)非常方便。


对象转换


现在我们已经足够了解以声明性和不可变样式处理对象。 让我们编写一个celebrateBirthday函数,以更新该人生日的年龄。


 const nextAge = compose(inc, prop('age')) const celebrateBirthday = person => assoc('age', nextAge(person), person) 

这是非常常见的模式。 就像我们在此处所做的那样,我们确实希望通过将函数应用于旧值来更改值,而不是使用新值更新属性。


我不知道拥有一种我们早已学过的工具,以较少的重复性和较不严格的样式编写此书的好方法。


Ramda再次为我们节省了演进功能。 evolve接受一个对象,并允许您为我们要更改的属性指定转换函数。 让我们来celebrateBirthday


 const celebrateBirthday = evolve({ age: inc }) 

这段代码说,我们将通过创建具有相同属性和值的新对象来转换指定的对象(由于强力样式而不会显示),但是age属性将通过将inc应用于age属性的原始值来获得。


evolve可以一次转换许多属性,甚至可以在多层嵌套中进行转换。 使用指定形式的转换函数,对象的转换可以具有与可变对象相同的图像,并且evolve将在结构之间递归传递。


请注意, evolve不会添加新属性。 如果为正在处理的对象中未发生的属性指定转换,则evolve只会忽略它。


我发现, evolve迅速成为我应用程序中的主力军。


合并物件


有时您需要将两个对象组合在一起。 典型的情况是,当您有一个使用命名选项的函数,并且想要将它们与默认选项结合在一起时。 Ramda为此提供了合并功能。


 function f(a, b, options = {}) { const defaultOptions = { value: 42, local: true } const finalOptions = merge(defaultOptions, options) } 

merge返回一个新对象,其中包含两个对象的所有属性和值。 如果两个对象具有相同的属性,则将获取第二个参数的值。


该规则与第二个论点的胜利使使用merge作为独立工具具有意义,但在输送机情况下意义不大。 在这种情况下,您通常需要为对象准备一系列转换,而这样的转换之一就是一些新属性值的并集。 在这种情况下,您将希望第一个参数获胜,而不是第二个。


尝试仅在管道中使用merge(newValues)不会给出我们想要的结果。


对于这种情况,我通常创建自己的实用程序reverseMerge 。 它可以写成const reverseMerge = flip(merge)flip调用交换适用于该函数的前两个参数。


merge执行表面合并。 如果对象组合在一起时具有其值为子对象的属性,则这些子对象不会合并。 Ramda目前不具备深度合并功能我要翻译原始文章已经有关于此主题的过时信息。如今, Ramda具有诸如mergeDeepLeft ,用于递归深度合并对象的mergeDeepRight以及其他用于合并的方法 )。


请注意, merge仅接受两个参数。 如果要将多个对象组合为一个,可以使用mergeAll ,它采用一组对象进行组合。


结论


今天,我们有了一套出色的工具,用于以声明性和不变的方式处理对象。 现在我们可以读取,添加,更新,删除和转换对象中的属性,而无需更改原始对象。 而且,我们可以采用一种轻松实现彼此组合功能的风格来完成所有这些事情。


下一个


现在我们可以使用不可变样式的对象,但是数组呢? “免疫和阵列”将告诉我们如何处理它们。

Source: https://habr.com/ru/post/zh-CN414337/


All Articles