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