使用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.lensPath
)R.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 仅提供 lensProp
、lensIndex
和 lensPath
。但是我们可以编写一个匹配 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”值,而不管级别如何。但它可能适合你的情况。
我正在尝试利用 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.lensPath
)R.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 仅提供 lensProp
、lensIndex
和 lensPath
。但是我们可以编写一个匹配 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”值,而不管级别如何。但它可能适合你的情况。