Рекурсивное слияние Рамды по совпадению ключей - PullRequest
1 голос
/ 21 февраля 2020

У меня есть два списка узлов, которые имеют такую ​​форму:

interface TreeNode {
    data: {
        name: string,
        sharedProp: boolean,
        oldProp: boolean
    },
    children: TreeNode[],
    parents: TreeNode[],
    thereAreSomeShallowProps: any,
}

Полный набор данных будет представлять собой массив TreeNode

Я хотел бы иметь функция, которую я могу пройти по этому дереву, объединяя изменения в дереве changes в базовое дерево. Некоторые функции, которые могут понадобиться:

  • Соответствуют значениям указанного ключа (в этом случае поддерживают многоуровневые ключи) и объединяют совпадения
    • , возможно, с flatten и groupBy
  • Когда значения объекта являются массивами, recurse
  • Устойчив к круговым ссылкам
  • Способен работать с очень большими объектами (более 100 000 узлов, по крайней мере)

Некоторые из функций, на которые я смотрел (но я не уверен, как связать воедино для создания нужной функции):

  • applySpec
  • groupBy
  • mergeWithKey
  • mergeDeepWithKey

Вот песочница для проверки с некоторыми тестами, которые должны лучше объяснить, чего я пытаюсь достичь

1 Ответ

1 голос
/ 21 февраля 2020

Хотя это может быть не самый лучший подход, его можно легко построить с помощью инструментов, которыми мы располагаем по дому. (В моем случае, с теми, которые я написал в другом ответе StackOverflow .) Я свободно использовал функции Ramda здесь, так как вопрос был помечен как Ramda (отказ от ответственности: я - автор Ramda), но ниже я показываю Альтернативная версия, которая создает необходимые функции утилиты с нуля.

Это предполагает, что ваш объект изменений будет и / или будет содержать разреженные массивы. Если нет, то как вы планируете сопоставить вещи?

Вот мой подход:

// Helper or utility functions
function * getPaths(o, p = []) {
  if (Object(o) !== o || Object .keys (o) .length == 0) yield p 
  if (Object(o) === o)
    for (let k of Object .keys (o))
      yield * getPaths (o[k], [... p, Number.isInteger (Number (k)) ? Number (k) : k])
}

const allPaths = (o) => [... getPaths(o)]

// Main function
const applyChanges = (obj, changes) =>
  reduce ((o, p) => assocPath (p, path (p, changes), o), obj, allPaths (changes))

// Sample data
const base = [
  {a: 1, b: {c: 11, d: [{e: 100}, {e: 111}]}},
  {a: 2, b: {c: 22, d: [{e: 200}, {e: 222}]}},
  {a: 3, b: {c: 33, d: [{e: 300}, {e: 333}]}},
]

const deltas = [
  {a: 8, b: {       d: [        , {e: 888}]}},
  ,
  {      b: {c: 99, d: [{e: 999},         ]}},
]

// Demonstration
console .log (
  applyChanges (base, deltas)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.js"></script>
<script> const {reduce, assocPath, path} = R                 </script>

allPaths находит пути ко всем конечным узлам в объекте, где индексы массива отображаются в виде чисел, а другие ключи в виде строк. Например,

const foo = {a: 42, b: {c: 12, d: [{e: 10}, {e: 20}]}}
allPaths (foo) //=> [["a"], ["b", "c"], ["b", "d", 0, "e"], ["b", "d", 1, "e"]]

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

Имея список путей в объекте изменений, мы можем затем применить значения, чтобы создать новую копию нашего Основной объект. Это сделано в applyChanges, нашей основной функции. Он находит пути в объекте changes, а затем использует assocPath и reduce Рамды, чтобы сложить их в наш основной объект.

Здесь у нас могут быть некоторые недостатки в скорости и памяти по двум причинам. Для скорости мы ищем значение на каждом пути, когда мы вызываем path(p, changes), но мы уже сделали соответствующий обход в getPath. Вероятно, будет некоторая экономия в отчете о другой структуре с path и value из getPath и последующем использовании их в applyChages. Это не влияет на сложность алгоритма c, а только на коэффициенты, и я не стал бы беспокоиться об этом, если бы у него не было измеримых проблем. Что касается памяти, этот стиль reduce с assocPath включает создание новых объектов на каждой итерации. Учитывая, что существует важное структурное разделение, это может не иметь большого значения, но для большого объекта changes это может быть проблемой. (Это не будет для меня серьезным беспокойством, но я держу такие вещи в затылке.)

Без Рамды

Поскольку я склонен думать в Рамде, я написал выше с помощью инструментов Ramda. Но есть только несколько функций. В этом случае R.reduce можно легко заменить на Array.prototype.reduce, и мы можем довольно легко написать наши собственные версии R.assocPath и R.path. Вот еще одна версия, которая не использует библиотеку:

// Utility functions

const isInt = Number.isInteger

const path = (ps = [], obj = {}) =>
  ps .reduce ((o, p) => (o || {}) [p], obj)

const assoc = (prop, val, obj) => 
  isInt (prop) && Array .isArray (obj)
    ? [... obj .slice (0, prop), val, ...obj .slice (prop + 1)]
    : {...obj, [prop]: val}

const assocPath = ([p = undefined, ...ps], val, obj) => 
  p == undefined
    ? obj
    : ps.length == 0
      ? assoc(p, val, obj)
      : assoc(p, assocPath(ps, val, obj[p] || (obj[p] = isInt(ps[0]) ? [] : {})), obj)


// Helper functions

function * getPaths(o, p = []) {
  if (Object(o) !== o || Object .keys (o) .length == 0) yield p 
  if (Object(o) === o)
    for (let k of Object .keys (o))
      yield * getPaths (o[k], [...p, isInt (Number (k)) ? Number (k) : k])
}

const allPaths = (o) => [... getPaths(o)]

// Main function
const applyChanges = (obj, changes) =>
  allPaths(changes).reduce((o, p) => assocPath(p, path(p, changes), o), obj)

// Sample data
const base = [
  {a: 1, b: {c: 11, d: [{e: 100}, {e: 111}]}},
  {a: 2, b: {c: 22, d: [{e: 200}, {e: 222}]}},
  {a: 3, b: {c: 33, d: [{e: 300}, {e: 333}]}},
]

const deltas = [
  {a: 8, b: {       d: [        , {e: 888}]}},
  ,
  {      b: {c: 99, d: [{e: 999},         ]}},
]

// Demonstration
console .log (
  applyChanges (base, deltas)
)

Прямой подход

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

...