我如何在 Ramda 中组合多个减速器?

How can I combine multiple reducers in Ramda?

我正在尝试通过组合几个不同的函数来构建报告。我已经能够使用一些香草 javascript 得到我想要的东西,但它太不稳定了,我知道如果我可以使用图书馆我会更好。 Ramda 似乎是正确的,但我遇到了障碍,如果有人能给我一个正确方向的推动,我将不胜感激。

我将从不同的文件中导入函数,并在最后一刻将它们拼接在一起以获得我需要的报告。

假设这是我的代码:

const data = [
  { name: 'fred', age: 30, hair: 'black' },
  { name: 'wilma', age: 28, hair: 'red' },
  { name: 'barney', age: 29, hair: 'blonde' },
  { name: 'betty', age: 26, hair: 'black' }
]
const partA = curry((acc, thing) => {
  if (!acc.names) acc.names = [];
  acc.names.push(thing.name);
  return acc;
})
const partB = curry((acc, thing) => {
  if (!acc.ages) acc.ages = [];
  acc.ages.push(thing.age);
  return acc;
})
const partC = curry((acc, thing) => {
  if (!acc.hairColors) acc.hairColors = [];
  acc.hairColors.push(thing.hair);
  return acc;
})

我似乎无法找到将 partA + partB + partC 函数压缩在一起的好方法,所以我得到了这个:

{
    ages: [30, 28, 29, 26],
    hairColors: ["black", "red", "blonde", "black"],
    names: ["fred", "wilma", "barney", "betty"]
}

这有效,但太糟糕了。

reduce(partC, reduce(partB, reduce(partA, {}, data), data), data)

这是我可以接受的一个,但我确定它不对。

const allThree = (acc, thing) => {
  return partC(partB(partA(acc, thing), thing), thing)
}
reduce(allThree, {}, data)

我已经尝试过 compose、pipe、reduce、reduceRight 和 into 以及其他一些方法,所以显然我在这里遗漏了一些非常基本的东西。

您可以使用 R.apply 规范来创建对象。对于每个 属性 使用 R.pluck:

从数组中获取值

const { pluck, applySpec } = R

const fn = applySpec({
  ages: pluck('age'),
  hairColors: pluck('hair'),
  named: pluck('name'),
})

const data =[{"name":"fred","age":30,"hair":"black"},{"name":"wilma","age":28,"hair":"red"},{"name":"barney","age":29,"hair":"blonde"},{"name":"betty","age":26,"hair":"black"}]

const result = fn(data)

console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous"></script>

更通用的版本接受具有未来 属性 名称的对象和要 pluck 的键,将对象映射到具有相关键的 curry pluck,然后将其与 R.applySpec 一起使用:

const { pipe, map, pluck, applySpec } = R

const getArrays = pipe(map(pluck), applySpec)

const fn = getArrays({ 'ages': 'age', hairColors: 'hair', names: 'name' })

const data =[{"name":"fred","age":30,"hair":"black"},{"name":"wilma","age":28,"hair":"red"},{"name":"barney","age":29,"hair":"blonde"},{"name":"betty","age":26,"hair":"black"}]

const result = fn(data)

console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous"></script>

您可以使用 Array.map() 对 vanilla JS 执行相同的操作,从数组的项目中提取一个值。要使用规范对象,请使用 Object.entries() 转换为条目,映射每个数组以获取相关值,然后使用 Object.fromEntries():

转换回对象

const getArrays = keys => arr => Object.fromEntries(
  Object.entries(keys)
    .map(([keyName, key]) => [keyName, arr.map(o => o[key])])
);   

const fn = getArrays({ 'ages': 'age', hairColors: 'hair', names: 'name' })

const data =[{"name":"fred","age":30,"hair":"black"},{"name":"wilma","age":28,"hair":"red"},{"name":"barney","age":29,"hair":"blonde"},{"name":"betty","age":26,"hair":"black"}]

const result = fn(data)

console.log(result)

我会使用 reducemergeWith:

您可以使用mergeWith合并两个对象。当发生冲突时(例如,两个对象中都存在一个键),一个函数将应用于两个值:

mergeWith(add, {a:1,b:2}, {a:10,b:10});
//=> {a:11,b:12}

因此在 reducer 中,您的第一个对象是累加器(以 {} 开头),第二个对象在每次迭代时从列表中取出。

unapply(flatten)(a, b) 技巧在幕后执行此操作:flatten([a, b])

const compact = reduce(mergeWith(unapply(flatten)), {});

console.log( compact([ { name: 'fred', age: 30, hair: 'black' }
                     , { name: 'wilma', age: 28, hair: 'red' }
                     , { name: 'barney', age: 29, hair: 'blonde' }
                     , { name: 'betty', age: 26, hair: 'black' }
                     ]
                    ));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous"></script>
<script>const {reduce, mergeWith, unapply, flatten} = R;</script>

你所说的可怕实际上是一般的方法,但它假设你的减少是相互独立的。在这种情况下,他们不是。

对于您描述的具体情况,我喜欢 reduce(mergeWith) 方法,但我会使用较少的技巧:

const compact = R.compose( R.reduce(R.mergeWith(R.concat))({}), R.map(R.map(R.of)) );

map(map(of))name: "fred" 更改为 name: ["fred"] 因此这些值可以与 concat.

合并

已经有几个很好的方法可以解决这个问题。 customcommander 和 jmw 的单行代码令人印象深刻。不过,我更喜欢 OriDrori 的 applySpec 解决方案,因为它看起来更明显(并且与其他两个不同,它允许您直接执行您请求的字段名称更改(“hair”=>“ hairColors”等)

但我们假设您确实更多地是在寻找如何使用这三个函数进行您想要的那种组合,仅作为示例。

它们没有按照您希望的方式组合的原因是它们都采用两个参数。您希望将不断变化的累加器和单独的事物传递给每个函数。典型的组合只传递一个参数(第一个调用的函数可能除外。)R.composeR.pipe 根本不会做你想做的事。

但是自己编写组合函数还是很简单的。我们称它为 recompose,然后像这样构建它:

const recompose = (...fns) => (a, b) => 
  fns .reduce ((v, fn) => fn (v, b), a)

const partA = curry((acc, thing) => {if (!acc.names) acc.names = []; acc.names.push(thing.name); return acc;})
const partB = curry((acc, thing) => {if (!acc.ages) acc.ages = []; acc.ages.push(thing.age); return acc;})
const partC = curry((acc, thing) => {if (!acc.hairColors) acc.hairColors = []; acc.hairColors.push(thing.hair); return acc;})

const compact = data => reduce (recompose (partA, partB, partC), {}, data)

const data = [{ name: 'fred', age: 30, hair: 'black' }, { name: 'wilma', age: 28, hair: 'red' }, { name: 'barney', age: 29, hair: 'blonde' }, { name: 'betty', age: 26, hair: 'black' }]

console .log (compact (data))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js"></script>
<script>const {reduce, curry} = R                                              </script>

recompose 函数将第二个参数传递给我们所有的组合函数。每个人都得到前面调用的结果(当然是从 a 开始)和 b.

的值

这可能就是您所需要的,但让我们注意一下有关此功能的一些事项。首先,虽然我们给它取了一个与compose同源的名字,但这实际上是pipe的一个版本。我们从第一个到最后一个调用函数。 compose 走向另一个方向。我们可以通过将 reduce 替换为 reduceRight 来轻松解决此问题。其次,我们可能想传递第三个参数,也许还有第四个,依此类推。如果我们处理它可能会很好。我们可以很容易地通过 rest 参数。

修复这两个问题,我们得到

const recompose = (...fns) => (a, ...b) => 
  fns .reduceRight ((v, fn) => fn (v, ...b), a)

这里还有一个潜在的问题。

这是必要的:

const compact = data => reduce (recompose (partA, partB, partC), {}, data)

即使使用 Ramda,我们传统上也是这样做的:

const compact = reduce (recompose (partA, partB, partC), {})

原因是你的reducing函数都修改了累加器。如果我们使用后者,然后 运行 compact (data),我们将得到

{
  ages: [30, 28, 29, 26], 
  hairColors: ["black", "red", "blonde", "black"], 
  names: ["fred", "wilma", "barney", "betty"]
}

很好,但如果我们再次调用它,我们会得到

{
  ages: [30, 28, 29, 26, 30, 28, 29, 26], 
  hairColors: ["black", "red", "blonde", "black", "black", "red", "blonde", "black"], 
  names: ["fred", "wilma", "barney", "betty", "fred", "wilma", "barney", "betty"]
}

这可能有点问题。 :-) 麻烦的是定义中只有一个累加器,这在 Ramda 中通常不是问题,但在这里当我们修改累加器时,我们会遇到真正的问题。因此,reducer 函数至少存在潜在问题。我也不需要看到它们上面的 curry 包装纸。

我建议将它们重写为 return 一个新值,而不是改变累加器。这是重写头发减少器的一种可能性:

const partC = (acc, {hair}) => ({
  ...acc, 
  hairColors: [...(acc.hairColors || []), hair]
})

我们应该注意到,这比原来的效率低,但明显更干净。


这个解决方案虽然使用了 Ramda,但做起来非常简单,实际上只使用了 reduce。我是 Ramda 的创始人之一,也是 Ramda 的忠实粉丝,但现代 JS 通常会减少对这样的库来解决此类问题的需求。 (另一方面,我可以看到 Ramda 采用 recompose 函数,因为它似乎通常很有用。)