4

There's an array in which there's a family tree in which there are names of people and their sex and the names of their parents(father&mother).

Now here I'm (unsuccessfully) trying to list the names of all children for a person in this family:

function allChildren(parent) {
  var children = ancestors.filter(
    function(person) {
      if (parent.sex == 'm') {
        return person.father == parent.name
      } else {
        return person.mother == parent.name
      }
    }
  )
  if (children.length == 0) {
    console.log(parent.name);
  } else {
    children.forEach(allChildren);
  }
}

Currently this is only console logging the people who have no children. The problem is, I need to push every child into an array of results but I'm using recursion, so I can't keep a results variable shared between all recursions.

(Of course I should also check that the same name has not been pushed before -assuming there are no repetitions- which is not my question and I know how to do it, my question is about getting an array of results, filled in recursion steps gradually(each recursion step can push an element in it)).

Luca Kiebel
  • 9,790
  • 7
  • 29
  • 44
aderchox
  • 3,163
  • 2
  • 28
  • 37
  • 1
    You may not be able to share a single `results` array between all recursive calls (passing it in as a parameter works but is cumbersome), but you should be able to create one array per function call that you `return`, and to which you push the results from each recursive call. – Bergi Aug 27 '18 at 18:38
  • @Bergi So that way I'll end up with an array of arrays?!? How should I use them then? should I concat them in the end? I know I could have the caller pass an array of results but I thought -as you've said- that's not a beautiful way. – aderchox Aug 27 '18 at 18:41
  • You may either end up with a nested structure representing your tree, or you concat them in every step so that the function returns a flat array. Depends on which you need. – Bergi Aug 27 '18 at 18:43
  • I need a flat array, could you please show me some hints so that I know how I could do it? (or if you could write an answer) I understand what you're saying in general but it's vague to me in detail. – aderchox Aug 27 '18 at 18:45
  • Maybe try how the code would look for nested arrays first. It's only a small step then to do the concatenation in the right place. – Bergi Aug 27 '18 at 18:49
  • 1
    Matching parents on `sex` and `name` is a bad way to design this function – a lookup for `{ name: "Bob", sex: "m" }` will get descendants of *all* males named Bob, not just the one in question. The problem, however, originates with your data. For example `{ ..., father: "Bob", mother: "Alice" }` does not adequately distinguish one Bob or Alice from a different Bob or Alice. – Mulan Aug 27 '18 at 20:14
  • Can you share some sample data? It's not simply an array of elements like `{name: 'Pebbles', sex: 'f', father: 'Fred', mother: 'Wilma'}`, is it? Because that wouldn't require recursion at all, only a filter. But that's how your description sounds. – Scott Sauyet Aug 27 '18 at 20:54
  • @ScottSauyet you mean iterate through all parents inside your filter function ? I can see it but it does not look simpler to me, can you post this solution ? – Logar Aug 27 '18 at 21:08
  • @Logar, I'm not sure about the data structure, but if it's as I describe, then just `(name) => ancestors.filter(child => child.father === name || child.mother === name)`, and if you want to reduce it to just names, `.map(p => p.name)`. This seems to easy, and I was wondering if I misunderstand the data structure. (Also it might be `(person)` for input; if so, then replace the standalone `name`s with `person.name`. This is still simple.) – Scott Sauyet Aug 27 '18 at 21:16
  • @ScottSauyet OP wants all descendents, not only direct children, that's why recursion seems more natural. I think the data structure is as you describe. – Logar Aug 27 '18 at 21:20
  • Ahh, that would make sense. – Scott Sauyet Aug 27 '18 at 21:23
  • 1
    @Narnia, are you looking for all *descendants* (children, grandchildren, great-grandchildren, etc.), not simply all *children*? – Scott Sauyet Aug 27 '18 at 21:28
  • what is wrong with using tail-recursion? That would make your live easier... – KarelG Aug 28 '18 at 06:34
  • @KarelG I'm a beginner and I honestly have no idea what a tail-recursion is, can you leave an answer? (I've come up with a solution by the way, should I post it as an answer?) – aderchox Aug 28 '18 at 06:42
  • 1
    a tail recursion is a recursion function _with an extra argument_ that is the result itself. More information can be read [here](https://stackoverflow.com/questions/33923/what-is-tail-recursion). So in essence, you just have `function allChildren(parent, response=[])` where response is your data-holder. You can just call `allChildren(json)` without problem. – KarelG Aug 28 '18 at 06:52
  • @Narnia is there something wrong with my answer ? Neither did you comment nor down/upvote, yet I believe it does answer your question – Logar Aug 28 '18 at 09:26
  • @KarelG: That is one way you might make a recursive function into a tail recursive one. But the basic definition of tail recursion is simply that the last step made by the function is the recursive call (thus the function returns the result of that call.) – Scott Sauyet Aug 28 '18 at 13:50

5 Answers5

4

Here is an approach similar to the one from KarelG. It is different enough, though, to warrant its own answer.

Differences

  • This one passes the ancestors as a parameter rather than keeping them as a free variable in the function. Free variables make code harder to understand and much harder to test.

  • It separates out the recursive function from the public function, keeping them together in a closure. This is a less common technique now that people are using some sort of module in many places, but it can still be useful, especially on the web. That is the (() => {/* ... return some function */ })() syntax.

  • It uses a Set as the recursive accumulator, to avoid duplicates.

  • It ignores the gender. I can't see a reason for checking it. If the child says that the mother is so-and-so, I don't see any point to double-checking that `mother.sex === 'f'``. I could be missing something here.

  • While it keeps the existing API of supplying a full person object and returning a list of names, it would be relatively easy to switch it to supplying just the names or to return a list of people. More on that below.

  • I named it allDescendents as that seemed more accurate than allChildren.

Code

const allDescendents = (() => {
  const all = (family, name, children = new Set()) => {
    const kids = family.filter(p => p.father === name || p.mother === name).map(k => k.name)
    kids.forEach(kid => (children.add(kid), all(family, kid, children)))
    return children
  }
  
  return (family, person) => Array.from(all(family, person.name))
})()

const bourbons = [ // https://en.wikipedia.org/wiki/Bourbon_family_tree
  {name: 'Philip III', sex: 'm', father: 'Louis IX', mother: 'Margaret'},
  {name: 'Robert', sex: 'm', father: 'Louis IX', mother: 'Margaret'},
  {name: 'Charles', sex: 'm', father: 'Philip III', mother: '?'},
  {name: 'Louis I', sex: 'm', father: 'Robert', mother: 'Beatrice'},
  {name: 'Philip IV', sex: 'm', father: 'Charles', mother: '?'},
  {name: 'Isabella', sex: 'f', father: 'Charles', mother: '?'},
  {name: 'Peter I', sex: 'm', father: 'Louis I', mother: 'Mary'},
  {name: 'James I', sex: 'm', father: 'Louis I', mother: 'Mary'},
  {name: 'John II', sex: 'm', father: 'Philip IV', mother: '?'},
  {name: 'Peter II', sex: 'm', father: 'James I', mother: 'Jeanne'},
  {name: 'John I', sex: 'm', father: 'James I', mother: 'Jeanne'},
  {name: 'Charles V', sex: 'm', father: 'John II', mother: '?'},
  {name: 'Joanna', sex: 'f', father: 'Peter I', mother: 'Isabella'},
  {name: 'Louis II', sex: 'f', father: 'Peter I', mother: 'Isabella'},
  {name: 'James II', sex: 'm', father: 'John I', mother: 'Catherine'},
  {name: 'Louis', sex: 'm', father: 'John I', mother: 'Catherine'},
  /* .. */
]

const louis1st = bourbons[3]

console.log(allDescendents(bourbons, louis1st))

Variants

This is factored in such a way as to make serious change fairly easy. Most of these changes can also be combined.

Return a Set

It might make more sense to return a Set of values rather than an Array. Presumably you do not want this collection to include duplicates. A Set is the right data structure for that. This solution uses a Set internally, and it's trivial to make that the public interface. Just remove the call to Array.from:

-   return (family, person) => Array.from(all(family, person.name))
+   return (family, person) => all(family, person.name)

Supply a Name

Since you're returning a collection of names, perhaps it would be easier to supply a name for the input. We can do this by altering the input parameter to receive the name:

-   return (family, person) => Array.from(all(family, person.name))
+   return (family, name) => Array.from(all(family, name))

Return People

Conversely, you might prefer to return the people objects directly. A list of names is only so helpful. A list containing the full Person object might be easier to work with. This is what that would look like:

-  const all = (family, name, children = new Set()) => {
-    const kids = family.filter(p => p.father === name || p.mother === name).map(k => k.name)
+  const all = (family, person, children = new Set()) => {
+   const kids = family.filter(p => p.father === person.name || p.mother === person.name)

-  return (family, person) => Array.from(all(family, person.name))
+  return (family, person) => Array.from(all(family, person))

Avoiding Default Parameters

If the default parameters in the function bother you (red squiggles in your editor, for instance), then you can move the parameters to the exported function. I like to keep the complexity in one place, which is why I have the defaults on the recursive function, but there is a good argument for simplifying the recursive function at the expense of the public function; it does make the recursive function somewhat easier to understand.

-   const all = (family, name, children = new Set()) => {
+   const all = (family, name, children) => {

-   return (family, person) => Array.from(all(family, person.name))
+   return (family, person) => Array.from(all(family, person.name, new Set()))

Making a module

Finally, we can easily turn this into a module. If you are deploying to the browser, this will still require a build step, but it does simplify the code: Here is one version:

const all = (family, name, children = new Set()) => {
  const kids = family.filter(p => p.father === name || p.mother === name).map(k => k.name)
  kids.forEach(kid => (children.add(kid), all(family, kid, children)))
  return children
}

const allDescendents = (family, person) => Array.from(all(family, person.name))

export {allDescendents}
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • 1
    Nice answer. It is kinda true that incest occurs frequently in that period. So yes, the use of `Set` is appropriate (for other readers, I am just giving a wink from his comment on my answer). However, I would return with objects instead of names directly if you want to re-use the function. Then map through the result list to get a list of names. Assume that someone wants to receive only male descendants. You can simply re-use that function and filter through the result list to exclude female persons. – KarelG Sep 04 '18 at 08:10
  • 1
    I agree that returning people is a more flexible and more useful version. I would have made that my default, except that the original seemed to want names. So I made it a variant instead. – Scott Sauyet Sep 04 '18 at 12:06
2

I have commented with

what is wrong with using tail-recursion? That would make your live easier...

What I have done is to write a function that does the task with a tail recursion. By using a second argument as data holder, you can pass the data array through your recursion tree. More information about tail recursion can be found in this post: What is tail recursion?

I have created a function (interactive example at bottom)

function showChildrenOf(person, children = []) {
  // ...
}

The children = [] means that an array [] is used as default argument if no argument got supplied. The concept is default parameters. So at the start of the recursion tree, you can simply call the function with showChildrenOf(person) to have an array of all of his descendants. In the function body, I am populating children if the person has children. Then I call the function again with showChildrenOf(child, children) so that the next function call happens with the child to check if he has children and a filled in array as data holder. Since I have provided an argument, the next function handling has children=[...] instead of an empty array.

const persons =
[
 {name: 'Who Am I', father: 'Dex Bob', mother: 'Alice Mania', g:'m'}
,{name: 'Mic Mac', father: 'Who Am I', mother: 'Mega Wonder', g:'m'}
,{name: 'Ani Mani', father: 'Bob Y', mother: 'Women Wonder', g:'f'}
,{name: 'Jan Jot', father: 'Who Am I', mother: 'Wunder Bra', g:'m'}
,{name: 'Kit Kat', father: 'Jan Jot', mother: 'Unknown', g:'m'}
,{name: 'Tiny Bell', father: 'Kit Kat', mother: 'Unknown', g:'f'}
,{name: 'Bill Kid', father: 'Kit Kat', mother: 'Unknown', g:'m'}
,{name: 'Billy Wuz', father: 'Oreos Oo', mother: 'Tiny Bell', g:'m'}
];

function showChildrenOf(person, children = []) {
  const tmp = persons.filter(p => (person.g === 'm' ? person.name === p.father : person.name === p.mother));
  if (tmp.length) {
    // has children
    children.push(...tmp);
    tmp.forEach(child => showChildrenOf(child, children));
  }
  else {
    // does not have children: do nothing
  }
  return children;
}

console.log(showChildrenOf(persons[0])); // showing 'Who Am I' children
console.log(showChildrenOf(persons[1])); // showing 'Mic Mac' children
KarelG
  • 5,176
  • 4
  • 33
  • 49
  • Thanks. But I'm not sure why but my editor (Intellij Idea) is making some parts of your code red. For example: ... , childrens = [ ] ) { ... – aderchox Aug 28 '18 at 08:00
  • 1
    @Narnia did you enable es6 syntax ? settings -> languages and frameworks -> javascript -> change javascript language version to ECMAScript 6 – Logar Aug 28 '18 at 09:33
  • @Narnia I have used ES2015 (aka ES6) features. IE is the only browser that does not support those default arguments. If you do not want inspection warnings/errors when using ES2015 + features in intellij, please follow Logar's suggestion. – KarelG Aug 28 '18 at 09:44
  • One minor flaw. If you add some incest to the mix, people will show up multiple times. (Say you add Gin Bean as the daughter of Jan Jot, and add Ping Pong as her son and change Billy Wuz's father to Ping Pong, you will see Billy Wuz appearing twice.) Maybe this is the reason first cousins aren't supposed to marry, but... You might fix this by using Sets rather than arrays for your output. – Scott Sauyet Aug 28 '18 at 13:43
1

Here is one short way to do it (if you can use arrow function and array destructuring, but you get the idea anyway) :

function allChildren(parent) {
    const children = ancestors.filter((person) => 
        (parent.sex === 'm' && person.father === parent.name) || (parent.sex === 'f' && person.mother === parent.name));
    return [
        parent, 
        ...children.reduce((grandChildren, child) => 
            [...grandChildren, ...allChildren(child)], [])
   ];
}
Logar
  • 1,248
  • 9
  • 17
  • You seemed to be looking for feedback on this. It would probably help if you made it a snippet, so that we could see it in action. – Scott Sauyet Aug 28 '18 at 13:46
  • @ScottSauyet, thanks for the feedback on the feedback request :) Well, while you're right on the fact that a snippet would be better, as the data structure is a bit long to replicate I expected OP to test the code, or at least to try to understand what it does. Usually people looking for an answer (and even casual readers) do this – Logar Aug 28 '18 at 14:15
  • Part of the reason I asked is that when I tried to run this on my own data, it didn't seem to work. The result seemed to yield something equivalent to `[ancestors]`, that is an array with one element, itself an array containing each person in `ancestors`. I asked for the snippet to see if you had a somewhat different input data structure. – Scott Sauyet Aug 28 '18 at 15:35
  • 1
    @ScottSauyet I just tested it against the sample that OP gives in his answer, here is the repl : https://repl.it/repls/SwelteringElementaryTelephone. It seems to work. One minor bug is that you will get the name of the original person included, but this could be easily fixed with wrapping the function in one that filters the original person out, and OP stated it's not a real life use case, he just wanted to play with recursion. – Logar Aug 28 '18 at 16:07
  • And I still won't make a snippet because the data sample is a bit large, and I don't feel like working on making a little sample from this one, sorry :) – Logar Aug 28 '18 at 16:07
  • My apologies. Whatever testing I did before on this was run improperly. However I combine your data or mine with my code or yours, the answers come out the same. I'm curious as to what went wrong before... but not *too* curious. The repl.it link is great. It didn't have to be a snippet, just some way to test. – Scott Sauyet Aug 28 '18 at 18:07
  • @ScottSauyet thanks for taking time to give feedback anyway, it doesn't feel great to get totally ignored :D Even if I get that without a snippet or a link to test against, it's a bit harder – Logar Aug 28 '18 at 19:47
1

Here's the solution I've come up with myself that works fine. It uses a recursive function and also makes use of the concept of closure in JavaScript.

So I've wrapped the recursive logic, I'd mentioned in my question earlier, in a separate inner function(which is named "all" -quickly and badly named, I know-) inside the main function(which is named "allChildren"):

function allChildren(parent) {
    var firstParentName = parent.name;
    var allChildrenArray = [];

    function all(parent) {
        var children = ancestors.filter(
            function(person) {
                if (parent.sex == 'm') {
                    return person.father == parent.name
                } else {
                    return person.mother == parent.name
                }
            }
        )
        if (children.length == 0) {
            if (allChildrenArray.indexOf(parent.name) == -1 &&
                parent.name != firstParentName) {
                allChildrenArray.push(parent.name);
            }
            return allChildrenArray;
        } else {
            children.map(function(child) {
                if (allChildrenArray.indexOf(child.name) == -1) {
                    allChildrenArray.push(child.name);
                }
            });
            children.forEach(all);
            return allChildrenArray;
        }
    }

    return all(parent);
}

Although, indeed, this is a useless function in reality. This was just a personal curiosity that I was interested in and I did it as an exercise to learn more about recursions. It's obviously a very common thing for a full-name to repeat in generations of a family tree.

Finally, calling this function using the data provided here, like this:

  console.log(allChildren(byName['Pauwels van Haverbeke'])); //byName is just a simple map from names to objects of the family tree. I haven't put the definition for the sake of brevity.

successfully gives this result:

(24) ["Lieven van Haverbeke", "Pieter Haverbeke", "Lieven Haverbeke", "Willem Haverbeke", "Daniel Haverbeke", "Jan Haverbeke", "Pieter Bernard Haverbeke", "Angela Haverbeke", "Jan Francies Haverbeke", "Pieter Antone Haverbeke", "Carel Haverbeke", "Carolus Haverbeke", "Emile Haverbeke", "Philibert Haverbeke", "Maria Haverbeke", "Livina Haverbeke", "Bernardus de Causmaecker", "Petronella de Decker", "Joanna de Causmaecker", "Maria van Brussel", "Elisabeth Haverbeke", "Laurentia Haverbeke", "Jacobus Bernardus van Brussel", "Jan Frans van Brussel"]
aderchox
  • 3,163
  • 2
  • 28
  • 37
  • This is applicable to the question and most of the answers here. Does the parent's gender actually matter here? That is, why not `return person.father === parent.name || person.mother === parent.name` ? – Scott Sauyet Aug 28 '18 at 13:53
  • 1
    Another comment: the use of `map` here is problematic. `map` should be used to transform one list to another and not for side-effects. The function passed to map should return the transformed value for the given element. If you are using side-effects, as you are doing here, you have `forEach`. – Scott Sauyet Aug 28 '18 at 13:58
-1

Now with a scoped function

function allChildren(parent) {
  var getChildrenRecursive = function(parent, elements){
    var children = ancestors.filter(
      function(person) {
        if (parent.sex == 'm') {
          return person.father == parent.name
        } else {
          return person.mother == parent.name
        }
      }
    )
    if (children.length == 0) {
      console.log(parent.name);
    } else {
      elements = elements.concat(children);
      children.forEach(function(child){elements = elements.concat(getChildrenRecursive(child, elements));})
      return elements;
    }
  };

  return getChildrenRecursive(parent, []);
}
  • 1
    Thanks but that makes using the function quite difficult which I think is opposite to what a function should do in the first place. – aderchox Aug 27 '18 at 18:42
  • You cannot share an array among the recursion steps if you don't do somthing like this, you can pass a parameter throgh steps, but this is not the question you did, so I edit my answer to do the same thing in a little more convenient way. – Eric Nordelo Galiano Aug 27 '18 at 18:46
  • Notice that `concat` does create a new array, instead of modifying the target. You may be looking for `push` – Bergi Aug 27 '18 at 19:20
  • Push add the entire array, not the elements, but you are right – Eric Nordelo Galiano Aug 27 '18 at 19:21
  • @Narnia, why is this function any more difficult to use than your own? It looks to have the same signature. – Scott Sauyet Aug 27 '18 at 21:30
  • 1
    @ScottSauyet This is the edited version(edited after I left that comment). Eric, thanks for your solution and help(I didn't give the negative), however your edited version is not working too, I've tested it on my data of 39 people in a family tree and it returns a result with more than 30thousand elements, most of them repetitive. I've written a solution myself which "seems" to be working, I'll put it as an answer now. (Thanks once again). – aderchox Aug 28 '18 at 06:39
  • Oh, that makes sense. Made some comments on your answer as well. – Scott Sauyet Aug 28 '18 at 14:25