使用 ramda 修改对象数组

Modify Array of Object using ramda

我有一个对象数组,如下所示,

[
{      
  "myValues": []
},
{
 "myValues": [],
 "values": [
    {"a": "x", "b": "1"},
    {"a": "y", "b": "2"}
  ],
  "availableValues": [],      
  "selectedValues": []
}
]

另外,如果我迭代对象,对象中存在的 "values" 键,它会将其转换为如下所示,

[
{      
  "myValues": []
},
{
  "myValues": [],
  "values": [
    {"a": "x", "b": "1"},
    {"a": "y", "b": "2"}
  ],
  "availableValues": [
    {"a": "x", "b": "1"},
    {"a": "y", "b": "2"}
  ],      
  "selectedValues": ["x", "y"]
}
]

我尝试使用 ramda 函数式编程但没有结果,

let findValues = x => {
  if(R.has('values')(x)){
  let availableValues = R.prop('values')(x);
  let selectedValues = R.pluck('a')(availableValues);
  R.map(R.evolve({
    availableValues: availableValues,
    selectedValues: selectedValues
  }))
 }
}
R.map(findValues, arrObj);

R.evolve 允许您通过将对象的值单独传递给所提供对象中的相应函数来更新对象的值,但它不允许您引用其他属性的值。

但是,R.applySpec 同样采用一个函数对象和 returns 一个新函数,该函数会将其参数传递给对象中提供的每个函数以创建一个新对象。这将允许您引用周围对象的其他属性。

我们还可以利用 R.when 仅在满足给定谓词时应用一些转换。

const input = [
  {      
    "myValues": []
  },
  {
    "myValues": [],
    "values": [
      {"a": "x", "b": "1"},
      {"a": "y", "b": "2"}
    ],
    "availableValues": [],      
    "selectedValues": []
  }
]

const fn = R.map(R.when(R.has('values'), R.applySpec({
  myValues:        R.prop('myValues'),
  values:          R.prop('values'),
  availableValues: R.prop('values'),
  selectedValues:  R.o(R.pluck('a'), R.prop('values'))
})))

const expected = [
  {      
    "myValues": []
  },
  {
    "myValues": [],
    "values": [
      {"a": "x", "b": "1"},
      {"a": "y", "b": "2"}
    ],
    "availableValues": [
      {"a": "x", "b": "1"},
      {"a": "y", "b": "2"}
    ],      
    "selectedValues": ["x", "y"]
  }
]

console.log(R.equals(fn(input), expected))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>

不变性

首先,你需要小心你的动词。 Ramda 不会修改你的数据结构。不变性是 Ramda 的核心,也是一般函数式编程的核心。如果你真的想改变你的数据,Ramda 将不是你的工具包。然而,Ramda 会对其进行转换,并且它可以以一种相对内存友好的方式进行转换,重用数据结构本身未转换的那些部分。

修正你的方法

让我们先看看清理函数中的几个问题:

let findValues = x => {
  if (R.has('values')(x)) {
    let availableValues = R.prop('values')(x);
    let selectedValues = R.pluck('a')(availableValues);
    R.map(R.evolve({
      availableValues: availableValues,
      selectedValues: selectedValues
    }))
  }
}

第一个也是最明显的问题是这个函数没有 return 任何东西。如上所述,Ramda 不会改变您的数据。所以一个没有 return 东西的函数在提供给 Ramda 的 map 时是无用的。我们可以通过 returning map(evolve) 调用的结果并在 if 条件失败时 returning 原始值来解决这个问题:

let findValues = x => {
  if (R.has('values')(x)) {
    let availableValues = R.prop('values')(x);
    let selectedValues = R.pluck('a')(availableValues);
    return R.map(R.evolve({
      availableValues: availableValues,
      selectedValues: selectedValues
    }))
  }
  return x;
}

接下来,map 调用毫无意义。 evolve 已经迭代了对象的属性。所以我们可以删除它。但我们还需要将 evolve 应用于您的输入值:

let findValues = x => {
  if (R.has('values')(x)){
    let availableValues = R.prop('values')(x);
    let selectedValues = R.pluck('a')(availableValues);
    return R.evolve({
      availableValues: availableValues,
      selectedValues: selectedValues
    }, x)
  }
  return x;
}

还有一个问题。 Evolve 需要一个包含转换值的函数的对象。例如,

evolve({
  a: n => n * 10,
  b: n => n + 5
})({a: 1, b: 2, c: 3}) //=> {a: 10, b: 7, c: 3}

请注意,提供给 evolve 的对象中的属性值为 函数 。此代码正在提供值。我们可以通过 availableValues: R.always(availableValues)R.always 包装这些值,但我认为使用参数为“_”的 lambda 更简单,这表示参数不重要:availableValues: _ => availableValues。您也可以写 availableValues: () => available values,但下划线表明通常的值被忽略。

解决这个问题得到一个有效的函数:

let findValues = x => {
  if (R.has('values')(x)){
    let availableValues = R.prop('values')(x);
    let selectedValues = R.pluck('a')(availableValues);
    return R.evolve({
      availableValues: _ => availableValues,
      selectedValues: _ => selectedValues
    }, x)
  }
  return x;
}

let arrObj = [{myValues: []}, {availableValues: [], myValues: [], selectedValues: [], values: [{a: "x", b: "1"}, {a: "y", b: "2"}]}];

console.log(R.map(findValues, arrObj))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.js"></script>

如果你 运行 这里的这个片段,你还会看到一个有趣的点,即结果 availableValues 属性 是对原始 values 的引用,不是它的单独副本。这有助于表明 Ramda 正在尽可能地重用您的原始数据。您还可以通过注意 R.map(findValues, arrObj)[0] === arrObj[0] //=> true.

来看到这一点

其他方法

我写了我自己的版本,而不是从你的开始。这是一个工作函数:

const findValues = when(
  has('values'),
  x => evolve({
    availableValues: _ => prop('values', x),
    selectedValues: _ => pluck('a', prop('values', x))
  }, x)
)

注意when, which captures the notion of "when this predicate is true for your value, use the following transformation; otherwise use your original value." We pass the predicate has('values'), essentially the same as above. Our transformation is similar to yours, using evolve, but it skips the temporary variables, at the minor cost of repeating the prop调用的使用。

关于这个版本,有些事情仍然困扰着我。将这些 lambda 与“_”一起使用实际上是对 evolve 的误用。我认为这更干净:

const findValues = when(
  has('values'),
  x => merge(x, {
    availableValues: prop('values', x),
    selectedValues: pluck('a', prop('values', x))
  })
)

这里我们使用merge,它是Object.assign的纯函数版本,将第二个对象的参数添加到第一个对象的参数中,当它们冲突时替换。

这是我会选择的。

花药溶液

自从我开始写这个答案后,Scott Christopher 发布了另一个,本质上是

const findValues = R.map(R.when(R.has('values'), R.applySpec({
  myValues:        R.prop('myValues'),
  values:          R.prop('values'),
  availableValues: R.prop('values'),
  selectedValues:  R.o(R.pluck('a'), R.prop('values'))
})))

这是一个很好的解决方案。它也很好地无意义。如果您的输入对象固定为这些属性,那么我会选择这个而不是我的 merge 版本。如果您的实际对象包含更多属性,或者只有在输入中有动态属性才应包含在输出中,那么我会选择我的 merge 解决方案。关键是 Scott 的解决方案对于这个特定结构更清晰,但我的解决方案更适合更复杂的对象。

循序渐进

这不是一个非常复杂的转换,但它很重要。构建这样的东西的一种简单方法是在非常小的阶段构建它。我经常在 Ramda REPL 中这样做,这样我就可以看到结果。

我的流程是这样的:

步骤 1

确保我只正确转换正确的值:

const findValues = when(
  has('values'), 
  always('foo')
)
map(findValues, arrObj) //=> [{myValues: []}, "foo"]

第 2 步

确保为我的转换器函数赋予正确的值。

const findValues = when(
  has('values'), 
  keys
)
map(findValues, arrObj) 
//=> [{myValues: []}, ["myValues", "values", "availableValues", "selectedValues"]]

这里identity would work just as well as keys。任何证明传递了正确对象的东西都可以。

步骤 3

在这里测试merge是否能正常工作。

const findValues = when(
  has('values'), 
  x => merge(x, {foo: 'bar'})
)
map(findValues, arrObj) 
//=> [
//     {myValues: []}, 
//     {
//       myValues: [], 
//       values: [{a: "x", b: "1"}, {a: "y", b: "2"}],
//       availableValues: [], 
//       selectedValues: [], 
//       foo: "bar"
//     }
//   ]

所以我可以正确地合并两个对象。请注意,这会保留我原始对象的所有现有属性。

步骤 4

现在,实际转换我想要的第一个值:

const findValues = when(
  has('values'),
  x => merge(x, {
    availableValues: prop('values', x)
  })
)
map(findValues, arrObj) 
//=> [
//     {myValues: []}, 
//     {
//       myValues: [], 
//       values: [{a: "x", b: "1"}, {a: "y", b: "2"}],
//       availableValues: [{a: "x", b: "1"}, {a: "y", b: "2"}],
//       selectedValues: []
//     }
//   ]

第 5 步

这就是我想要的。所以现在添加另一个 属性:

const findValues = when(
  has('values'),
  x => merge(x, {
    availableValues: prop('values', x),
    selectedValues: pluck('a', prop('values', x))
  })
)
map(findValues, arrObj) 
//=> [
//     {myValues: []}, 
//     {
//       myValues: [], 
//       values: [{a: "x", b: "1"}, a: "y", b: "2"}],
//       availableValues: [{a: "x", b" "1"}, {a: "y", b: "2"}], 
//       selectedValues: ["x", "y"]
//     }
//   ]

这终于给出了我想要的结果。

这是一个快速的过程。我可以 运行 并在 REPL 中快速测试,迭代我的解决方案,直到我得到满足我要求的东西。