Ramda js:针对嵌套的对象和嵌套的对象数组的镜头

20
使用 Ramda.js(和 lens),我希望修改下面的 JavaScript 对象,将 ID 为 "/1/B/i" 的对象中的 "NAME:VERSION1" 更改为 "NAME:VERSION2"。
我想使用 lens,因为我只想更改一个深度嵌套的值,但否则保留整个结构不变。
我不想使用 lensIndex,因为我永远不知道数组的顺序是什么,所以我想通过查找其"id"字段在数组中“查找”对象。
我可以使用 lens 来完成这个操作吗?还是应该用另一种方式?
{
  "id": "/1",
  "groups": [
    {
      "id": "/1/A",
      "apps": [
        {
          "id": "/1/A/i",
          "more nested data skipped to simplify the example": {} 
        }
      ]
    },
    {
      "id": "/1/B",
      "apps": [
        { "id": "/1/B/n", "container": {} },
        {
          "id": "/1/B/i",

          "container": {
            "docker": {
              "image": "NAME:VERSION1",
              "otherStuff": {}
            }
          }
        }
      ]
    }

  ]
}
2个回答

28

通过创建一个根据ID匹配对象的镜头,然后与其他镜头组合以钻取到图像字段,应该是可能的。

首先,我们可以创建一个镜头,它将聚焦于与某个断言相匹配的数组元素(注意:仅当保证至少匹配列表中的一个元素时,此镜头才有效)。

//:: (a -> Boolean) -> Lens [a] a
const lensMatching = pred => (toF => entities => {
    const index = R.findIndex(pred, entities);
    return R.map(entity => R.update(index, entity, entities),
                 toF(entities[index]));
});

请注意,这里我们手动构建镜头而不是使用R.lens来避免查找与谓词匹配的项目的重复。

有了这个函数,我们可以构建一个匹配给定ID的镜头。

//:: String -> Lens [{ id: String }] { id: String }
const lensById = R.compose(lensMatching, R.propEq('id'))

然后我们可以将所有镜头组合在一起,以对准图像字段

const imageLens = R.compose(
  R.lensProp('groups'),
  lensById('/1/B'),
  R.lensProp('apps'),
  lensById('/1/B/i'),
  R.lensPath(['container', 'docker', 'image'])
)

可以使用以下方式更新data对象:

set(imageLens, 'NAME:VERSION2', data)

如果你愿意的话,你可以更进一步地声明一个聚焦于图像字符串版本的镜头。

const vLens = R.lens(
  R.compose(R.nth(1), R.split(':')),
  (version, str) => R.replace(/:.*/, ':' + version, str)
)

set(vLens, 'v2', 'NAME:v1') // 'NAME:v2'

然后可以将此附加到imageLens的组合中,以针对整个对象中的版本。

const verLens = compose(imageLens, vLens);
set(verLens, 'VERSION2', data);

2
这真的很容易理解,可以轻松地组合和/或修改。谢谢!对于lensMatching,是否可以替换为:`function lensMatching(pred) { return R.lens( R.find(pred), (newVal, array, other) => { const index = R.findIndex(pred, array); return R.update(index, newVal, array); } ) }`对我来说,这似乎更容易与镜头文档相关联。但是,我有什么遗漏吗? - Greg Edwards
@GregEdwards 那也应该可以。我建议另一种实现的主要原因是为了防止两次扫描数组(一次在 find 中,一次在 findIndex 中),但如果数组比较小,这不应该是问题。 - Scott Christopher
1
感谢您的解决方案,@ScottChristopher :) 我完全是ramda和函数式编程的新手,但这不是ramda中缺少的功能吗?-通过属性值匹配镜头?我认为这是一个非常常见的情况,并且希望能够直接将最终的compose函数编写为数组,如下所示:const imageLens = R.lensPath(['groups', {id: '/1/B'}, 'apps', {id: '/1/B/i'}, 'container', 'docker', 'image']) - aweibell
1
@aweibell Ramda提供的原语可以构建许多镜头(以及其他功能),这些镜头并未包含在库中。对于像所提出的这样的镜头,情况会有点棘手,因为您需要确保该镜头始终与其焦点匹配,而通用库无法保证这一点(例如,对于空数组,它将匹配什么?) - Scott Christopher
@ScottChristopher 好的,我认为这个问题也会出现在包含函数中,比如有效(据我所知)的 lensPath(['groups', 1, 'apps', 1, 'container', 'docker', 'image']) - aweibell
1
@aweibell 您说得对,像 R.lensIndex 这样的函数属于类似的范畴,尽管它们的实用性被认为是超过了非完整性的潜在风险。如果您对于这个函数有着相似的感觉,那么我建议您在 ramda/ramda github 存储库上提出问题或 PR,以向更广泛的 Ramda 社区提出这个想法。 - Scott Christopher

10

这里有一个解决方案:

const updateDockerImageName =
R.over(R.lensProp('groups'),
       R.map(R.over(R.lensProp('apps'),
                    R.map(R.when(R.propEq('id', '/1/B/i'),
                                 R.over(R.lensPath(['container', 'docker', 'image']),
                                        R.replace(/^NAME:VERSION1$/, 'NAME:VERSION2')))))));

当然,这可以拆分为更小的函数。 :)


有没有不需要如此深度嵌套查询的方法?使用“compose”或其他方式可以吗? - Greg Edwards
2
感谢您的回答,让我们更清楚如何使用over、map和when。 - Greg Edwards

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接