Вложенные функции сокращения / рекурсия / функциональное программирование / обход дерева - PullRequest
0 голосов
/ 15 октября 2018

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

import { curry } from 'lodash/fp'
import { fromJS } from 'immutable'

const reduce = curry((fn, acc, it) => it.reduce(fn, acc))

describe('reduceNested', () => {
  const input = fromJS({
    a1: {
      b1: {
        c1: {
          d1: {
            e1: 'one',
            e2: 'two',
            e3: 'three'
          },
          d2: {
            e1: 'one',
            e2: 'two',
            e3: 'three'
          }
        },
        c2: {
          d1: {
            e1: 'one',
            e2: 'two'
          }
        }
      }
    },
    a2: {
      b1: {
        c1: {
          d1: {
            e1: 'one'
          },
          d2: {
            e1: 'one'
          }
        }
      },
      b2: {
        c1: {
          d1: {
            e1: 'one'
          },
          d2: {
            e1: 'one'
          }
        }
      }
    },
    a3: {
      b1: {
        c1: {}
      }
    }
  })

  const expected = fromJS({
    one: [
      'a1.b1.c1.d1.e1',
      'a1.b1.c1.d2.e1',
      'a1.b1.c2.d1.e1',
      'a2.b1.c1.d1.e1',
      'a2.b1.c1.d2.e1',
      'a2.b2.c1.d1.e1',
      'a2.b2.c1.d2.e1'
    ],
    two: ['a1.b1.c1.d1.e2', 'a1.b1.c1.d2.e2', 'a1.b1.c2.d1.e2'],
    three: ['a1.b1.c1.d1.e3', 'a1.b1.c1.d2.e3']
  })

  const init = fromJS({ one: [], two: [], three: [] })

  test('madness', () => {
    const result = reduce(
      (acc2, val, key) =>
        reduce(
          (acc3, val2, key2) =>
            reduce(
              (acc4, val3, key3) =>
                reduce(
                  (acc5, val4, key4) =>
                    reduce(
                      (acc6, val5, key5) =>
                        acc6.update(val5, i =>
                          i.push(`${key}.${key2}.${key3}.${key4}.${key5}`)
                        ),
                      acc5,
                      val4
                    ),
                  acc4,
                  val3
                ),
              acc3,
              val2
            ),
          acc2,
          val
        ),
      init,
      input
    )

    expect(result).toEqual(expected)
  })

  test('better', () => {
    const result = reduceNested(
      (acc, curr, a, b, c, d, e) =>
        acc.update(curr, i => i.push(`${a}.${b}.${c}.${d}.${e}`)),
      init,
      input
    )

    expect(result).toEqual(expected)
  })
})

Я хотел бы написать функцию reduceNested, которая достигает того же результата, но без всех вложенных функций сокращения.В lodash/fp я не вижу ничего похожего на адрес, поэтому я подумал о создании новой функции reduceNested и добавлении переменных к обратному вызову для каждого ключа в дереве.Я пытался реализовать реальную логику, но, к сожалению, пока что застрял.Я знаю, reduceNested нужно будет использовать fn.length, чтобы определить, как далеко от источника сверлить, но кроме этого я просто застрял.

const reduceNested = curry((fn, acc, iter) => {
  // TODO --> use (fn.length - 2)
})

Ответы [ 4 ]

0 голосов
/ 15 октября 2018

функциональный стиль

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

const reduceTree = (proc, state, tree, path = []) =>
  reduce                        // call reduce with:
    ( (acc, [ key, value ]) =>  // reducer
        isObject (value)               // value is an object (another tree):
          ? reduceTree                 //   recur with:
              ( proc                   //     the proc
              , acc                    //     the acc
              , value                  //     this value (the tree)
              , append (path, key)     //     add this key to the path
              )                        // value is NOT an object (non-tree):
          : proc                       //   call the proc with:
              ( acc                    //     the acc
              , value                  //     this value (non-tree, plain value)
              , append (path, key)     //     add this key to the path
              )
    , state                     // initial input state 
    , Object.entries (tree)     // [ key, value ] pairs of input tree
    )

Свободные значения выше определены для использования префиксной нотации, которая более знакома в функциональном стиле -

const isObject = x =>
  Object (x) === x

const reduce = (proc, state, arr) =>
  arr .reduce (proc, state)

const append = (xs, x) =>
  xs .concat ([ x ])

Теперь у нас есть общая reduceTree функция -

const result =
  reduceTree
    ( (acc, value, path) =>           // reducer
        [ ...acc, { path, value } ] 
    , []                              // initial state
    , input                           // input tree
    )

console.log (result)
// [ { path: [ 'a1', 'b1', 'c1', 'd1', 'e1' ], value: 'one' }
// , { path: [ 'a1', 'b1', 'c1', 'd1', 'e2' ], value: 'two' }
// , { path: [ 'a1', 'b1', 'c1', 'd1', 'e3' ], value: 'three' }
// , { path: [ 'a1', 'b1', 'c1', 'd2', 'e1' ], value: 'one' }
// , { path: [ 'a1', 'b1', 'c1', 'd2', 'e2' ], value: 'two' }
// , { path: [ 'a1', 'b1', 'c1', 'd2', 'e3' ], value: 'three' }
// , { path: [ 'a1', 'b1', 'c2', 'd1', 'e1' ], value: 'one' }
// , { path: [ 'a1', 'b1', 'c2', 'd1', 'e2' ], value: 'two' }
// , { path: [ 'a2', 'b1', 'c1', 'd1', 'e1' ], value: 'one' }
// , { path: [ 'a2', 'b1', 'c1', 'd2', 'e1' ], value: 'one' }
// , { path: [ 'a2', 'b2', 'c1', 'd1', 'e1' ], value: 'one' }
// , { path: [ 'a2', 'b2', 'c1', 'd2', 'e1' ], value: 'one' } 
// ]

Мы можем формировать результат результата так, как нам нравится -

const result =
  reduceTree
    ( (acc, value, path) =>                        // reducer
        ({ ...acc, [ path .join ('.') ]: value })
    , {}                                           // initial state
    , input                                        // input tree
    )

console.log (result)
// { 'a1.b1.c1.d1.e1': 'one'
// , 'a1.b1.c1.d1.e2': 'two'
// , 'a1.b1.c1.d1.e3': 'three'
// , 'a1.b1.c1.d2.e1': 'one'
// , 'a1.b1.c1.d2.e2': 'two'
// , 'a1.b1.c1.d2.e3': 'three'
// , 'a1.b1.c2.d1.e1': 'one'
// , 'a1.b1.c2.d1.e2': 'two'
// , 'a2.b1.c1.d1.e1': 'one'
// , 'a2.b1.c1.d2.e1': 'one'
// , 'a2.b2.c1.d1.e1': 'one'
// , 'a2.b2.c1.d2.e1': 'one'
// }

input для нашего теста необходимо продемонстрировать, что reduceTree работает для различных уровней вложенности -

test ('better', () => {
  const input =
    { a: { b: { c: 1, d: 2 } }, e: 3 }

  const expected =
    { 'a.b.c': 1, 'a.b.d': 2, e: 3 }

  const result =
    reduceTree
      ( (acc, value, path) =>
          ({ ...acc, [ path .join ('.') ]: value })
      , {}
      , input 
      )

  expect(result).toEqual(expected)
})

Наконец, убедитесь, что программа работает в вашем браузере ниже -

const isObject = x =>
  Object (x) === x

const reduce = (proc, state, arr) =>
  arr .reduce (proc, state)

const append = (xs, x) =>
  xs .concat ([ x ])

const reduceTree = (proc, state, tree, path = []) =>
  reduce
    ( (acc, [ key, value ]) =>
        isObject (value)
          ? reduceTree
              ( proc
              , acc
              , value
              , append (path, key)
              )
          : proc
              ( acc
              , value
              , append (path, key)
              )
    , state
    , Object.entries (tree)
    )

const input =
  { a: { b: { c: 1, d: 2 } }, e: 3 }

const result =
  reduceTree
    ( (acc, value, path) =>
        [ ...acc, { path, value } ]
    , []
    , input
    )

console.log (result)
// { 'a.b.c': 1, 'a.b.d': 2, e: 3 }

… с помощью друзей

Генераторы императивного стиля облегчают работу такого рода задач, в то время какпредлагая интуитивно понятный язык для описания предполагаемого процесса.Ниже мы добавляем traverse, который генерирует [ path, value ] пар для вложенного tree (объект) -

const traverse = function* (tree = {}, path = [])
{ for (const [ key, value ] of Object.entries (tree))
    if (isObject (value))
      yield* traverse (value, append (path, key))
    else
      yield [ append (path, key), value ]
}

Используя Array.from, мы можем подключить генератор напрямую к нашему существующему функционалу reduce;reduceTree теперь просто специализация -

const reduceTree = (proc, state, tree) =>
  reduce
    ( (acc, [ path, value ]) =>
        proc (acc, value, path)
    , state
    , Array.from (traverse (tree))
    )

Сайт вызова тот же -

const input =
  { a: { b: { c: 1, d: 2 } }, e: 3 }

const result =
  reduceTree
    ( (acc, value, path) =>
        ({ ...acc, [ path .join ('.') ]: value })
    , {}
    , input
    )

console.log (result)
// { 'a.b.c': 1, 'a.b.d': 2, e: 3 }

Проверьте результат в вашем браузере ниже -

const isObject = x =>
  Object (x) === x

const reduce = (proc, state, arr) =>
  arr .reduce (proc, state)

const append = (xs, x) =>
  xs .concat ([ x ])

const traverse = function* (tree = {}, path = [])
{ for (const [ key, value ] of Object.entries (tree))
    if (isObject (value))
      yield* traverse (value, append (path, key))
    else
      yield [ append (path, key), value ]
}

const reduceTree = (proc, state, tree) =>
  reduce
    ( (acc, [ path, value ]) =>
        proc (acc, value, path)
    , state
    , Array.from (traverse (tree))
    )

const input =
  { a: { b: { c: 1, d: 2 } }, e: 3 }

const result =
  reduceTree
    ( (acc, value, path) =>
        ({ ...acc, [ path .join ('.') ]: value })
    , {}
    , input
    )

console.log (result)
// { 'a.b.c': 1, 'a.b.d': 2, e: 3 }
0 голосов
/ 15 октября 2018

Я бы решил эту проблему, используя рекурсивную функцию генератора

В этом примере я создал отдельную функцию childPathsAndValues.Здесь мы достигли разделения интересов: эта функция не должна знать, что вы добавляете каждый путь к массиву.Он просто пересекает объект и возвращает комбинации путь / значение.

function* childPathsAndValues(o) {
   for(let k in o) {
     if(typeof(o[k]) === 'object') {
	  for(let [childPath, value] of childPathsAndValues(o[k])) {
  	      yield [`${k}.${childPath}`, value];
          }
     } else {
        yield [k, o[k]];
     }
  }
}

const input = {"a1":{"b1":{"c1":{"d1":{"e1":"one","e2":"two","e3":"three"},"d2":{"e1":"one","e2":"two","e3":"three"}},"c2":{"d1":{"e1":"one","e2":"two"}}}},"a2":{"b1":{"c1":{"d1":{"e1":"one"},"d2":{"e1":"one"}}},"b2":{"c1":{"d1":{"e1":"one"},"d2":{"e1":"one"}}}},"a3":{"b1":{"c1":{}}}};
  
const acc = {};
for(let [path, value] of childPathsAndValues(input)) {
   console.log(`${path} = ${value}`);
   acc[value] = acc[value] || [];
   acc[value].push(path);
}

console.log('*** Final Result ***');
console.log(acc);
0 голосов
/ 15 октября 2018

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

Vanilla Javascript:

import { curry, __ } from 'lodash/fp'
const reduce = require('lodash/fp').reduce.convert({ cap: false })
reduce.placeholder = __


const reduceNested = curry((fn, acc, iter, paths) =>
  reduce(
    (acc2, curr, key) =>
      paths.length === fn.length - 3
        ? fn(acc2, curr, ...paths, key)
        : reduceNested(fn, acc2, curr, [...paths, key]),
    acc,
    iter
  )
)

export default reduceNested

Использование:

test('better', () => {
  const result = reduceNested(
    (acc, curr, a, b, c, d, e) => ({
      ...acc,
      [curr]: [...acc[curr], `${a}.${b}.${c}.${d}.${e}`]
    }),
    init,
    input,
    []
  )

  expect(result).toEqual(expected)
})

С Immutable.js:

import { curry } from 'lodash/fp'

const reduce = curry((fn, acc, it) => it.reduce(fn, acc))

const reduceNested = curry((fn, acc, iter, paths) =>
  reduce(
    (acc2, curr, key) =>
      paths.size === fn.length - 3
        ? fn(acc2, curr, ...paths, key)
        : reduceNested(fn, acc2, curr, paths.push(key)),
    acc,
    iter
  )
)

export default reduceNested

Использование:

test('better', () => {
  const result = reduceNested(
    (acc, curr, a, b, c, d, e) =>
      acc.update(curr, i => i.push(`${a}.${b}.${c}.${d}.${e}`)),
    init,
    input,
    List()
  )

  expect(result).toEqual(expected)
})
0 голосов
/ 15 октября 2018

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

function traverse(input, acc, path = []) {                       // path will be used internally so you don't need to pass it to get from the outside, thus it has a default value
    Object.keys(input).forEach(key => {                          // for each key in the input
        let newPath = [...path, key];                            // the new path is the old one + the current key
        if(input[key] && typeof input[key] === "object") {       // if the current value (at this path) is an object
            traverse(input[key], acc, newPath);                  // traverse it using the current object as input, the same accumulator and the new path
        } else {                                                 // otherwise (it's not an object)
            if(acc.hasOwnProperty(input[key])) {                 // then check if our accumulator expects this value to be accumulated
                acc[input[key]].push(newPath.join('.'));         // if so, add its path to the according array
            }
        }
    });
}

let input = {"a1":{"b1":{"c1":{"d1":{"e1":"one","e2":"two","e3":"three"},"d2":{"e1":"one","e2":"two","e3":"three"}},"c2":{"d1":{"e1":"one","e2":"two"}}}},"a2":{"b1":{"c1":{"d1":{"e1":"one"},"d2":{"e1":"one"}}},"b2":{"c1":{"d1":{"e1":"one"},"d2":{"e1":"one"}}}},"a3":{"b1":{"c1":{}}}};
let acc = { one: [], two: [], three: [] };

traverse(input, acc);

console.log(acc);
...