When dealing with complex traversals, generator functions can offer a simplified solution. By creating a straightforward generator and then wrapping it in a function to generate an array, the process becomes more streamlined:
const getPaths = function * (xs, ps = []) {
for (let x of xs) {
yield [... ps, x .attributes .name] .join (' > ')
yield * getPaths(x .children, [...ps, x .attributes .name])
}
}
const categoryNames = (categories) =>
[... getPaths (categories .data)]
const categories = {data: [{type: "categories", id: "1", attributes: {name: "Top Level"}, children: [{type: "categories", id: "2", attributes: {name: "Sub 1"}, children: [{type: "categories", id: "4", attributes: {name: "Sub 1-2"}, children: []}]}, {type: "categories", id: "3", attributes: {name: "Sub 2"}, children: []}]}]};\
console .log (
categoryNames(categories)
)
getPaths
could be enhanced by removing the join (' > ')
call and incorporating it within a map
call at the end of categoryNames
. However, since the specifics surrounding children
and attributes.names
are quite tailored to this problem, making it more generic may not be necessary.
Clarification
If you found the provided code confusing, here's an explanation to help clarify it. Please bear with me if some parts are already clear to you. Let's break it down:
The main function, categoryNames
, simply acts as a wrapper around getPaths
, which does most of the heavy lifting.
Two key aspects to note about getPaths
:
It is a generator function, indicated by the *
between the function
keyword and its arguments. This allows it to create Generator objects that adhere to the iterable protocol, enabling usage in constructs like let x of generator
and [...generator]
. Through this, categoryNames
converts the output of getPaths
into an array. Generator functions operate by yielding individual values or using yield * anotherGenerator
to produce each value yielded by another generator separately. The function gets suspended between these yield
calls until the next value request.
It is a recursive function; the function body invokes itself again with simpler parameters. Typically, recursive functions include an explicit base case where a direct answer is returned without further recursion when the input simplifies sufficiently. Here, the base case is implicit - when xs
becomes an empty array, the loop body never triggers, halting the recursion.
getPaths
takes a list of values (
xs</code commonly represents such lists) and an array of strings representing current hierarchy paths up to the node being processed. For instance, this might contain <code>["Top Level", "Sub 1"]
. Note that the latter is a
default parameter; if left unspecified, it defaults to an empty array.
The function loops over supplied values. For each one, it yields a result by combining current paths with the name
property from the attribute
property of the current object, separated by " > "
. It then recursively processes the child nodes, yielding each child's children sequentially while maintaining the path records. A slightly optimized and clearer version of this approach could look like:
const getPaths = function * (xs, paths = []) {
for (let x of xs) {
const newPaths = [... paths, x .attributes .name]
yield newPaths .join (' > ')
yield * getPaths (x .children, newPaths)
}
}
Alternatively, you could define newPaths
as follows:
const newPaths = paths .concat (node .attributes .name)
I hope this breakdown provides clarity. If you have any questions, feel free to leave a comment.