Ramda中是否有添加基于多属性计算的对象属性的惯用写法?
这是个很棒的问题!很多Ramda用户都会遇到类似的需求——R.evolve擅长用函数更新已有属性,但要基于多个现有属性新增一个衍生属性时,它就不太顺手了。
首先得夸夸你自己实现的assocDerived:
const assocDerived = R.curry((name, f, obj) => ({ ...obj, [name]: f(obj) }));
这个函数完全符合Ramda的设计哲学:柯里化、纯函数、把关注点拆分得清清楚楚。你用它封装assocFruitTotal的方式也非常地道:
const sumFruit = R.pipe(R.props(['appleCount', 'orangeCount']), R.sum); const assocFruitTotal = assocDerived('fruitCount', sumFruit);
高频使用它完全没问题——这正是函数式编程中“封装常用组合逻辑”的好习惯。
为什么Ramda没有原生提供这个功能?
Ramda的核心设计理念是提供基础的、可组合的原语,而不是覆盖所有可能的业务场景。assocDerived本质上是R.assoc(给对象添加属性)和“接收整个对象计算值”的逻辑组合,Ramda更希望用户通过基础函数自行拼接出符合需求的工具——毕竟衍生属性的逻辑千差万别(有的要求和、有的要拼接字符串、有的要做复杂转换),内置一个特定函数反而会限制灵活性。
更优的Ramda惯用写法
如果你想用Ramda的原生函数组合出类似效果,这里有几种常用方案:
方案1:用R.converge组合R.assoc
converge可以把多个分支函数的结果传递给主函数,非常适合这种“从原对象同时取原数据和衍生值”的场景:
const assocFruitTotal = R.converge(R.assoc('fruitCount'), [ // 第一个分支:计算衍生值 R.pipe(R.props(['appleCount', 'orangeCount']), R.sum), // 第二个分支:保留原对象 R.identity ]);
调用时assocFruitTotal({ appleCount: 5, orangeCount: 3 })就会返回预期的结果。
方案2:用R.juxt+R.merge拆分合并
juxt会对同一个输入执行多个函数,返回结果数组,再用merge合并原对象和新属性对象:
const assocFruitTotal = R.pipe( R.juxt([ R.identity, // 保留原对象 // 生成只包含新属性的对象 R.pipe(R.props(['appleCount', 'orangeCount']), R.sum, R.objOf('fruitCount')) ]), R.merge // 合并两个对象 );
这个写法的优势是可以轻松扩展成添加多个衍生属性——只要在juxt里加更多生成新属性对象的分支即可。
方案3:用R.evolve的小技巧(不推荐作为首选)
如果硬要用上R.evolve,可以先给对象初始化衍生属性,再用evolve计算它的值(注意evolve的回调可以接收第二个参数——整个对象):
const assocFruitTotal = R.pipe( R.assoc('fruitCount', 0), // 先初始化属性 R.evolve({ fruitCount: (_, obj) => R.sum(R.props(['appleCount', 'orangeCount'], obj)) }) );
不过这个写法不够直观,不如前两种方案清晰,所以更适合在已有evolve链里插入衍生属性的场景,而不是单独使用。
总结
你自己写的assocDerived是完全合理且高效的实现,高频使用它完全没问题。如果想贴合Ramda的原生组合风格,上面的converge或juxt方案都是不错的选择——本质上都是用基础原语拼接出符合需求的逻辑,这也是函数式编程的乐趣所在。
内容的提问来源于stack exchange,提问作者slim

