Как я могу объединить несколько редукторов в Ramda?

#javascript #functional-programming #ramda.js

#javascript #функциональное программирование #ramda.js

Вопрос:

Я пытаюсь создавать отчеты, комбинируя несколько разных функций. Я смог получить то, что я хочу, используя какой-то ванильный 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, а также некоторые другие, поэтому, очевидно, я упускаю что-то довольно фундаментальное здесь.

Ответ №1:

Вы можете использовать R.применить спецификацию для создания объекта. Для каждого свойства получаем значения из массива с помощью 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> 

Более общая версия принимает объект с именем свойства future и ключом для 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> 

Вы можете сделать то же самое с помощью vanilla JS, используя Array.map() для извлечения значения из элементов массива. Чтобы использовать объект спецификации, преобразуйте в записи с помощью 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) 

Ответ №2:

Я бы использовал reduce с mergeWith :

Вы можете использовать mergeWith для объединения двух объектов. При возникновении коллизии (например, ключ существует в обоих объектах) функция применяется к обоим значениям:

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

Итак, в редукторе вашим первым объектом является накопитель (начинается с {} ), а второй объект берется из списка на каждой итерации.

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> 

Ответ №3:

То, что вы описываете как ужасное, на самом деле является общим способом сделать это, но это предполагает, что ваши сокращения не зависят друг от друга. В данном случае это не так.

Для конкретного случая, который вы описываете, мне нравится этот 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

Ответ №4:

Уже есть несколько хороших способов решить эту проблему. Однострочники из customcommander и jmw весьма впечатляют. Однако я предпочитаю applySpec решение от OriDrori, поскольку оно кажется гораздо более очевидным, что происходит (и, в отличие от двух других, оно позволяет вам напрямую изменять запрашиваемое вами изменение имени поля («hair» => «hairColors» и т. Д.)

Но давайте предположим, что вы действительно больше ищете, как создать желаемую композицию, используя эти три функции только в качестве примеров.

Причина, по которой они составляются не так, как вам хотелось бы, заключается в том, что все они принимают два параметра. Вы хотите передать изменяющийся накопитель и отдельную вещь каждой функции. Типичная композиция передает только один параметр (за исключением, возможно, первой вызванной функции). R.compose и R.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)
 

несмотря на то, что с Рамдой мы традиционно делаем это:

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

Причина в том, что все ваши восстановительные функции модифицируют аккумулятор. Если бы мы использовали последний, а затем запустили 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 не является проблемой, но здесь, когда мы модифицируем аккумулятор, мы можем получить реальные проблемы. Таким образом, существует, по крайней мере, потенциальная проблема с функциями редуктора. Также нет необходимости, чтобы я мог видеть для curry них оболочку.

Я бы предложил переписать их, чтобы возвращать новое значение, а не изменять накопитель. Вот одна из возможностей переписать редуктор волос:

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

Мы должны отметить, что это менее эффективно, чем оригинал, но значительно чище.


Это решение, хотя и использует Ramda, делает это очень легко, на самом деле используя только reduce . Я один из основателей Ramda и большой поклонник, но современный JS часто уменьшает потребность в подобной библиотеке для решения такого рода проблем. (С другой стороны, я мог видеть, что Ramda использует эту recompose функцию, поскольку она кажется в целом полезной.)

Комментарии:

1. Я подумал, что заменю свой первоначальный ответ. Функция recompose, по-видимому, является здесь правильным решением. Я хочу отправить накопитель через свой собственный набор функций, потому что это просто сокращенные версии реальных функций, которые мне нужны. Я не думал о побочных эффектах модификации оригинального аккумулятора на каждом этапе. Спасибо за этот исчерпывающий ответ.