3

I have a nested data structure, and I want to create a recursive function that, given an object's name parameter, will return the parent object's name paramter.

There are several related questions, however, the answers don't explain why my function getParentName isn't working.

Why is getParentName not working?

const nestedData = {
  name: "parent",
  children: [{ name: "child", children: [{ name: "grandchild" }] }],
};

function getParentName(nested, name) {
  if (nested.children && nested.children.map((d) => d.name).includes(name)) {
    return nested.name;
  } else if (nested.children) {
    nested.children.forEach((child) => {
      return getParentName(child, name);
    });
  }
  return undefined; //if not found
}

//The parent of "grandchild" is "child" - but the function returns undefined
const parentName = getParentName(nestedData, "grandchild");

Why does this function not find the parent?

NicoWheat
  • 2,157
  • 2
  • 26
  • 34

2 Answers2

4

The problem with your answer is .forEach ignores the return value. There is no return for your else if branch. .forEach is for side effects only. Consider using a generator which makes it easier to express your solution -

function* getParentName({ name, children = [] }, query) {
  for (const child of children)
    if (child.name === query)
      yield name
    else
      yield *getParentName(child, query)
}

const data = {
  name: "parent",
  children: [{ name: "child", children: [{ name: "grandchild" }] }],
}

const [result1] = getParentName(data, "grandchild")
const [result2] = getParentName(data, "foobar")
const [result3] = getParentName(data, "parent")

console.log("result1", result1)
console.log("result2", result2)
console.log("result3", result3)

The answer will be undefined if no matching node is found or if a matched node does not have a parent -

result1 child
result2 undefined
result3 undefined

Notice [] is needed to capture a single result. This is because generators can return 1 or more values. If you do not like this syntax, you can write a generic first function which gets only the first value out of generator -

function first(it) {
  for (const v of it)
    return v
}
const result1 = first(getParentName(data, "grandchild"))
const result2 = first(getParentName(data, "foobar"))
const result3 = first(getParentName(data, "parent"))

The advantages of this approach are numerous. Your attempt uses .map and .includes both of which iterate through children completely. In the other branch, .forEach is used which exhaustively iterates through all children as well. This approach avoids unnecessary .map and .includes but also stops immediately after the first value is read.

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Wow. I knew Array.forEach() method doesn't return a value, but I did not know it would ignore deliberate internal return statements. Typescript should complain about returning within forEach :/ – NicoWheat Apr 17 '22 at 05:50
  • 1
    the `return` you wrote belongs to an ordinary function used as a “callback.” this only signals to `.forEach` to move on to the next element. there may be a way to enable stricter TS behaviour to warn when void functions return a value. another option would be to abandon TS in favor of a superior (sound) type system like ReScript. – Mulan Apr 17 '22 at 14:02
0

@Mulan answered my question, stating that the function failed because return statements inside .forEach() are ignored. They then offered a generator function as a superior alternative.

For the sake of clarity of comparison, here is a minimally altered form of the original function that works. forEach() was replaced with a (for x of array) loop. In addition only truthy values are returned.


function getParentName(nested, name) {
  if (nested.children && nested.children.some((d) => d.name === id)) {
    return nested.name;
  } else if (nested.children) {
    for (const child of node.children) {
      const result = getParentName(child, id);
      if (result) return result;
    }
  }
  return undefined; //if not found
}

NicoWheat
  • 2,157
  • 2
  • 26
  • 34
  • 1
    Very nice exercise to take, good work. A couple things I would point out: 1) `nested.children.some(d => d.name === name)` is another way of writing `nested.children.map(d => d.name).includes(name)` that only iterates through `nested.children` once. 2) `for (var i = 0; ...) { return ... }` will immediately return the first `getParentName(...)`, and `i` will never increment to the second, third item and so so on. 3) `for (const d of children) { ... }` is a way you can avoid writing `for` loops that require fragile `i`-based incrementing and indexing. – Mulan Apr 18 '22 at 03:12
  • 1
    to fix the premature `return`, you can write `for (...) { const result = getParentName(...); if (result) return result; }` this will only cause the function to return if `getParentName` returns an actual value, otherwise `for` will continue iterating :D – Mulan Apr 18 '22 at 03:14
  • I will update my answer with your suggestions. – NicoWheat Apr 18 '22 at 21:56