Сопоставление массива объектов вместе с родительским свойством с помощью Ramda

#javascript #functional-programming #ramda.js

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

Вопрос:

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

 inventories = [
  {
    id: 'Berlin',
    products: [{ sku: '123', amount: 99 }],
  },
  {
    id: 'Paris',
    products: [
      { sku: '456', amount: 3 },
      { sku: '789', amount: 777 },
    ],
  },
]
 

Что я хочу сделать, так это преобразовать его в плоский список продуктов, который содержит дополнительную информацию, такую как inventoryId , inventoryIndex , и productIndex .

 products = [
  { inventoryId: 'Berlin', inventoryIndex: 1, sku: '123', amount: 99, productIndex: 1 },
  { inventoryId: 'Paris', inventoryIndex: 2, sku: '456', amount: 3, productIndex: 1 },
  { inventoryId: 'Paris', inventoryIndex: 2, sku: '789', amount: 777, productIndex: 2 },
]
 

Как я писал ранее, выполнение этого императивным способом не является проблемой.

 function enrichProductsWithInventoryId(inventories) {
  const products = []
  for (const [inventoryIndex, inventory] of inventories.entries()) {
    for (const [productIndex, product] of inventory.products.entries()) {
      product.inventoryId = inventory.id
      product.inventoryIndex = inventoryIndex   1
      product.productIndex = productIndex   1
      products.push(product)
    }
  }

  return products
}
 

Проблема в том, что я пытаюсь решить это с помощью ramda. Я понятия не имею, как получить доступ inventoryId при отображении продуктов. Было бы здорово увидеть фрагмент кода, написанный с использованием ramda, который выполняет то же самое, что и приведенный выше.

Приветствия, Томаш

Ответ №1:

Вы можете сделать это легко с помощью flatMap .

 const inventories = [
    {
        id: "Berlin",
        products: [{ sku: "123", amount: 99 }]
    },
    {
        id: "Paris",
        products: [
            { sku: "456", amount: 3 },
            { sku: "789", amount: 777 }
        ]
    }
];

const products = inventories.flatMap((inventory, inventoryIndex) =>
    inventory.products.map((product, productIndex) => ({
        inventoryId: inventory.id,
        inventoryIndex: inventoryIndex   1,
        sku: product.sku,
        amount: product.amount,
        productIndex: productIndex   1
    })));

console.log(products); 

Обратите внимание, что flatMap это вызывается chain в Ramda, но вам нужно addIndex будет к нему.

 const inventories = [
    {
        id: "Berlin",
        products: [{ sku: "123", amount: 99 }]
    },
    {
        id: "Paris",
        products: [
            { sku: "456", amount: 3 },
            { sku: "789", amount: 777 }
        ]
    }
];

const chainIndexed = R.addIndex(R.chain);
const mapIndexed = R.addIndex(R.map);

const products = chainIndexed((inventory, inventoryIndex) =>
    mapIndexed((product, productIndex) => ({
        inventoryId: inventory.id,
        inventoryIndex: inventoryIndex   1,
        sku: product.sku,
        amount: product.amount,
        productIndex: productIndex   1
    }), inventory.products), inventories);

console.log(products); 
 <script src="https://unpkg.com/ramda@0.27.1/dist/ramda.min.js"></script> 

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

1. Хотя это работает, не должны ли ваши внутренние вызовы логически быть map s вместо flatMap / chainIndexed ?

2. @ScottSauyet Хороший улов. Не знаю, почему это не вызвало ошибку типа.

3. Потому что это JS, и вещи, которые не должны работать, вроде как работают … пока они этого не делают!

4. Спасибо, ребята, это работает как шарм. У меня есть только один дополнительный вопрос. Можете ли вы рассматривать внутреннюю mapIndexed функцию как чистую, когда inventory для построения объекта используются «внешние» и inventoryIndex переменные?

5. @TomaszLempart Да, это чистая функция.

Ответ №2:

Я бы, вероятно, сделал это аналогично тому, что предложил Aadit, хотя я бы сформулировал это немного по-другому и использовал деструктурирование параметров

 const convert = (inventories) =>
  inventories .flatMap (({id: inventoryId, products}, i, _, inventoryIndex = i   1) =>
    products.map (
      ({sku, amount}, i, _, productIndex = i   1) => 
        ({inventoryId, inventoryIndex, sku, amount, productIndex})
    )
  )

const inventories = [{id: "Berlin", products: [{sku: "123", amount: 99}]}, {id: "Paris", products: [{sku: "456", amount: 3}, {sku: "789", amount: 777}]}]

console .log (convert (inventories)); 
 .as-console-wrapper {max-height: 100% !important; top: 0} 

Но, если вы хотите разбить это на более мелкие составные части, Ramda может помочь вам написать их и склеить в единое целое. Если мы попытаемся описать, что мы делаем, мы можем увидеть это как четыре шага. Мы переименовываем id поле в inventoryId , добавляем текущий индекс в наши инвентаризации и отдельные индексы для каждой группы products , и мы денормализуем / выравниваем наши вложенные списки, способствуя products слиянию массивов с их родителями.

Все это потенциально повторно используемые преобразования. Если бы мы захотели, мы могли бы выделить для них отдельные вспомогательные функции, а затем использовать Ramda для объединения их в единую функцию. Это может выглядеть так:

 const {pipe, toPairs, map, fromPairs, addIndex, chain, merge, evolve} = R

const mapKeys = (cfg) => pipe (
  toPairs,
  map (([k, v]) => [cfg [k] || k, v]),
  fromPairs
)
 
const addOrdinals = (name) => 
  addIndex (map) ((x, i) => ({... x, [name]: i   1 }))

const promote = (name) => 
  chain (({[name]: children, ...rest}) => map (merge(rest), children))

const transform = pipe (
  map (mapKeys ({id: 'inventoryId'})),
  addOrdinals ('inventoryIndex'),
  map (evolve ({products: addOrdinals ('productIndex')})),
  promote ('products')
)

const inventories = [{id: "Berlin", products: [{sku: "123", amount: 99}]}, {id: "Paris", products: [{sku: "456", amount: 3}, {sku: "789", amount: 777}]}]

console .log (transform (inventories)) 
 .as-console-wrapper {max-height: 100% !important; top: 0} 
 <script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js"></script> 

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