嵌套归约函数/递归/函数式编程/树遍历
Nested Reduce Functions / Recursion / Functional Programming / Tree Traversal
我一直 运行 处于最终嵌套很多 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
来实现相同的结果,但没有所有嵌套的 reduce 函数。我在 lodash/fp
中没有看到任何东西或类似于地址,所以我的想法是创建一个新函数 reduceNested
并为树中的每个键的回调添加变量。我已经尝试实现实际的逻辑,但不幸的是暂时被卡住了。我知道 reduceNested
将需要使用 fn.length
来确定要钻入源头的深度,但除此之外我只是被卡住了。
const reduceNested = curry((fn, acc, iter) => {
// TODO --> use (fn.length - 2)
})
您可以使用非常适合这种遍历的递归,如下所示:
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);
我会用递归的方法解决这个问题generator function
在这个例子中,我创建了一个单独的函数 childPathsAndValues
。在这里,我们实现了关注点分离:此函数不需要知道您将每个路径附加到一个数组。它只是遍历对象和 returns 的 path/value 组合。
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);
正如其他答案所指出的,递归是关键;但是,与其编写和重写会改变数据的程序代码,而且需要针对每种情况手动编写工具,为什么不在需要时使用和重用此功能。
原版 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)
})
functional style
You were on the right track with your answer, however recurring based on the user-supplied procedure's length is a misstep. Instead, the variable-length path should be passed as a single, variable-length value – an array
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
)
Free values above are defined to use prefix notation, which is more familiar in functional style –
const isObject = x =>
Object (x) === x
const reduce = (proc, state, arr) =>
arr .reduce (proc, state)
const append = (xs, x) =>
xs .concat ([ x ])
Now we have a generic reduceTree
function –
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' }
// ]
We can shape the output of the result however we like –
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'
// }
The input
for our test should demonstrate that reduceTree
works for various levels of nesting –
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)
})
Lastly, verify the program works in your browser below –
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 }
… with the help of some friends
Imperative-style generators make light work of this kind of task while offering an intuitive language to describe the intended process. Below we add traverse
which generates [ path, value ]
pairs for a nested tree
(object) –
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 ]
}
Using Array.from
we can plug the generator directly into our existing functional reduce
; reduceTree
is now just a specialization –
const reduceTree = (proc, state, tree) =>
reduce
( (acc, [ path, value ]) =>
proc (acc, value, path)
, state
, Array.from (traverse (tree))
)
The call site is the same –
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 }
Verify the result in your browser below –
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 }
我一直 运行 处于最终嵌套很多 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
来实现相同的结果,但没有所有嵌套的 reduce 函数。我在 lodash/fp
中没有看到任何东西或类似于地址,所以我的想法是创建一个新函数 reduceNested
并为树中的每个键的回调添加变量。我已经尝试实现实际的逻辑,但不幸的是暂时被卡住了。我知道 reduceNested
将需要使用 fn.length
来确定要钻入源头的深度,但除此之外我只是被卡住了。
const reduceNested = curry((fn, acc, iter) => {
// TODO --> use (fn.length - 2)
})
您可以使用非常适合这种遍历的递归,如下所示:
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);
我会用递归的方法解决这个问题generator function
在这个例子中,我创建了一个单独的函数 childPathsAndValues
。在这里,我们实现了关注点分离:此函数不需要知道您将每个路径附加到一个数组。它只是遍历对象和 returns 的 path/value 组合。
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);
正如其他答案所指出的,递归是关键;但是,与其编写和重写会改变数据的程序代码,而且需要针对每种情况手动编写工具,为什么不在需要时使用和重用此功能。
原版 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)
})
functional style
You were on the right track with your answer, however recurring based on the user-supplied procedure's length is a misstep. Instead, the variable-length path should be passed as a single, variable-length value – an array
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
)
Free values above are defined to use prefix notation, which is more familiar in functional style –
const isObject = x =>
Object (x) === x
const reduce = (proc, state, arr) =>
arr .reduce (proc, state)
const append = (xs, x) =>
xs .concat ([ x ])
Now we have a generic reduceTree
function –
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' }
// ]
We can shape the output of the result however we like –
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'
// }
The input
for our test should demonstrate that reduceTree
works for various levels of nesting –
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)
})
Lastly, verify the program works in your browser below –
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 }
… with the help of some friends
Imperative-style generators make light work of this kind of task while offering an intuitive language to describe the intended process. Below we add traverse
which generates [ path, value ]
pairs for a nested tree
(object) –
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 ]
}
Using Array.from
we can plug the generator directly into our existing functional reduce
; reduceTree
is now just a specialization –
const reduceTree = (proc, state, tree) =>
reduce
( (acc, [ path, value ]) =>
proc (acc, value, path)
, state
, Array.from (traverse (tree))
)
The call site is the same –
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 }
Verify the result in your browser below –
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 }