使用ramda.js,如何替换嵌套结构中的值?

Using ramda.js, how to replace a value in a nested structure?

我正在尝试利用 将对象中的值替换为 ramda.js。与链接引用不同,我的对象有更多的嵌套层,所以它失败了。

在下面的例子中,我们有一个详细描述城市景点的对象。首先它指定了城市,然后我们深入到 nyc,然后是 zoos,然后是 StatenIslandZoo,最后我们到达 zooInfo,它拥有两种动物的两条记录。在每一个中,我们在与 animal 键关联的值中都有 animl 的名称。我想通过将其替换为另一个字符串和 return 整个 cityAttractions 对象的新副本来更正该值的字符串。

const cityAttractions = {
    "cities": {
        "nyc": {
            "towers": ["One World Trade Center", "Central Park Tower", "Empire State Building"],
            "zoos": {
                "CentralParkZoo": {},
                "BronxZoo": {},
                "StatenIslandZoo": {
                    "zooInfo": [
                        {
                            "animal": "zebra_typo", // <- replace with "zebra"
                            "weight": 100
                        },
                        {
                            "animal": "wrongstring_lion", // <- replace with "lion"
                            "weight": 1005
                        }
                    ]
                }
            }
        },
        "sf": {},
        "dc": {}
    }
}

所以我定义了一个非常类似于的函数:

const R = require("ramda")

const myAlter = (myPath, whereValueEquals, replaceWith, obj) => R.map(
    R.when(R.pathEq(myPath, whereValueEquals), R.assocPath(myPath, replaceWith)),
    obj
)

然后调用 myAlter() 并将输出存储到 altered:

const altered = myAlter(["cities", "nyc", "zoos", "StatenIslandZoo", "zooInfo", "animal"], "zebra_typo", "zebra", cityAttractions)

但是在检查时,我发现没有发生任何替换:

console.log(altered.cities.nyc.zoos.StatenIslandZoo.zooInfo)
// [
//   { animal: 'zebra_typo', weight: 100 },
//   { animal: 'wrongstring_lion', weight: 1005 }
// ]

一些故障排除
如果我们返回并检查原始 cityAttractions 对象,那么我们可以先提取 cityAttractions.cities.nyc.zoos.StatenIslandZoo.zooInfo 的级别,然后使用 myAlter() 对其进行操作。

const ZooinfoExtraction = R.path(["cities", "nyc", "zoos", "StatenIslandZoo", "zooInfo"])(cityAttractions)
console.log(ZooinfoExtraction)
// [
//   { animal: 'zebra_typo', weight: 100 },
//   { animal: 'wrongstring_lion', weight: 1005 }
// ]

console.log(myAlter(["animal"], "zebra_typo", "zebra", ZooinfoExtraction))
// here it works!
// [
//   { animal: 'zebra', weight: 100 },
//   { animal: 'wrongstring_lion', weight: 1005 }
// ]

因此,出于某种原因,myAlter() 适用于提取的 ZooinfoExtraction 但不适用于原始 cityAttractions。这是个问题,因为我需要整个原始结构(只是替换指定的值)。


编辑 - 故障排除 2


我想问题在于

R.path(["cities", "nyc", "zoos", "StatenIslandZoo", "zooInfo", "animal"], cityAttractions)

returns undefined.

主要问题是 animal 属性 是数组项的一部分。由于数组索引应该是一个数字,所以 Zebra 的路径实际上是:

["cities", "nyc", "zoos", "StatenIslandZoo", "zooInfo", 0, "animal"]

但是,这将迫使您知道实际索引。

此外,映射数组 return 是数组的克隆(经过更改),而不是整个结构。

要解决此问题,您可以使用透镜(在本例中为R.lensPathR.over到return整个结构的更新克隆。

示例:

const { curry, over, lensPath, map, when, pathEq, assoc } = R
  
const alterAnimal = curry((path, subPath, whereValueEquals, replaceWith, obj) =>
  over(
    lensPath(path), 
    map(when(pathEq(subPath, whereValueEquals), assoc(subPath, replaceWith))),
    obj
  ))

const cityAttractions = {"cities":{"nyc":{"towers":["One World Trade Center","Central Park Tower","Empire State Building"],"zoos":{"CentralParkZoo":{},"BronxZoo":{},"StatenIslandZoo":{"zooInfo":[{"animal":"zebra_typo","weight":100},{"animal":"wrongstring_lion","weight":1005}]}}},"sf":{},"dc":{}}}

const altered = alterAnimal(
  ["cities", "nyc", "zoos", "StatenIslandZoo", "zooInfo"],
  ["animal"],
  "zebra_typo", 
  "zebra", 
  cityAttractions
)

console.log(altered)
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js" integrity="sha512-t0vPcE8ynwIFovsylwUuLPIbdhDj6fav2prN9fEu/VYBupsmrmk9x43Hvnt+Mgn2h5YPSJOk7PMo9zIeGedD1A==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

既然你转换了一个对象的属性值,你也可以使用R.evolve,并提供一个可以涵盖所有情况的更新函数。例如:

const { curry, over, lensPath, map, evolve, flip, prop, __ } = R
  
const alterObj = curry((updateFn, prop, path, obj) =>
  over(
    lensPath(path), 
    map(evolve({
      [prop]: updateFn
    })),
    obj
  ))
  
const replacements = {
  'zebra_typo': 'zebra',
  'wrongstring_lion': 'lion',
}
  
const alterAnimals = alterObj(prop(__, replacements))

const cityAttractions = {"cities":{"nyc":{"towers":["One World Trade Center","Central Park Tower","Empire State Building"],"zoos":{"CentralParkZoo":{},"BronxZoo":{},"StatenIslandZoo":{"zooInfo":[{"animal":"zebra_typo","weight":100},{"animal":"wrongstring_lion","weight":1005}]}}},"sf":{},"dc":{}}}

const altered = alterAnimals(
  "animal",
  ["cities", "nyc", "zoos", "StatenIslandZoo", "zooInfo"],
  cityAttractions
)

console.log(altered)
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js" integrity="sha512-t0vPcE8ynwIFovsylwUuLPIbdhDj6fav2prN9fEu/VYBupsmrmk9x43Hvnt+Mgn2h5YPSJOk7PMo9zIeGedD1A==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

使用镜头

另一种基于lens的方法是编写一个新的lens函数。 Ramda 仅提供 lensProplensIndexlensPath。但是我们可以编写一个匹配 animal 匹配的第一个数组元素的。我可以重用我在其他答案中使用过的 lensMatch 函数,然后使用 const animalLens = lensMatch ('animal') 对其进行配置。然后我们可以将其与其他镜头合成以获得我们想要更改的 属性。它可能看起来像这样:

const lensMatch = (propName) => (key) => lens ( 
  find (propEq (propName, key)),
  (val, arr, idx = findIndex (propEq (propName, key), arr)) =>
      update (idx > -1 ? idx : length (arr), val, arr)
)

const animalLens = lensMatch ('animal')

const updateAnimalName = (oldName, newName, attractions) =>  set (compose (
  lensPath (['cities', 'nyc', 'zoos', 'StatenIslandZoo', 'zooInfo']), 
  animalLens (oldName), 
  lensProp ('animal')
), newName, attractions)

const cityAttractions = {cities: {nyc: {towers: ["One World Trade Center", "Central Park Tower", "Empire State Building"], zoos: {CentralParkZoo: {}, BronxZoo: {}, StatenIslandZoo: {zooInfo: [{animal: "zebra_typo", weight: 100}, {animal: "wrongstring_lion", weight: 1005}]}}}, sf: {}, dc: {}}}

console .log (
  updateAnimalName ('zebra_typo', 'zebra', cityAttractions)
)
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js"></script>
<script> const {lens, find, propEq, findIndex, update, length, set, lensPath, compose, lensProp} = R </script>

显然,如果您愿意,我们可以将其折叠到多个动物名称(比如斑马和狮子)上。

通用替换函数

另一种完全不同的方法是——如果拼写错误值不太可能出现在你的数据结构中的其他地方——简单地遍历整个树,将所有 "zebra_typo" 替换为 "zebra"。那将是一个简单的递归:

const replaceVal = (oldVal, newVal) => (o) =>
  o == oldVal 
    ? newVal
  : Array .isArray (o)
    ? o .map (replaceVal (oldVal, newVal))
  : Object (o) === o
    ? Object .fromEntries (Object .entries (o) .map (([k, v]) => [k, replaceVal (oldVal, newVal) (v)]))
  : o


const cityAttractions = {cities: {nyc: {towers: ["One World Trade Center", "Central Park Tower", "Empire State Building"], zoos: {CentralParkZoo: {}, BronxZoo: {}, StatenIslandZoo: {zooInfo: [{animal: "zebra_typo", weight: 100}, {animal: "wrongstring_lion", weight: 1005}]}}}, sf: {}, dc: {}}}

console .log (
  replaceVal ('zebra_typo', 'zebra') (cityAttractions)
)
.as-console-wrapper {max-height: 100% !important; top: 0}

这种方法非常通用,但更针对于将所有“foo”值替换为“bar”值,而不管级别如何。但它可能适合你的情况。