2

Forgive me if this is too much of a general question but I seem to run into this problem frequently when I need to develop recursive algorithms which update some value on each recursive call. Please take this example below:

I want to make a recursive algorithm that accumulates the ages of the given profile and all the kids and kids of kids etc that exist within that profile.

Profile structure:

const profile = {
  name: 'peter',
  age: 56,
  kids: [{
    name: 'jill',
    age: 23,
    kids: [{
      name: 'jeter',
      age: 1
    }, {
      name: 'bill',
      age: 2
    }]
  }]
}

If I declare a variable outside of the scope of the function and update that it works. Like so:

let totalage = 0
function getAge(profile) {
  totalage = profile.age + totalage
  if(!profile.kids) return
  for(const kid of profile.kids) {
    getAge(kid)
  }
}
getAge( profile)
console.log(totalage)

By this logic, to make the function more reusable I want to make a shell function that sets up the age variable so it doesn't have to be globally declared any time I want to get all the ages, but this solution returns 0, the original age value:

function getAges(profile) {
  let age = 0
  recurs( profile, age)
  return age
}

function recurs(profile, age) {
  age = profile.age + age
  if(!profile.kids) return
  for(const kid of profile.kids) {
    recurs(kid, age)
  }
}
console.log(getAges(profile))

Any idea what is wrong?

Nguyễn Văn Phong
  • 13,506
  • 17
  • 39
  • 56
Tamjid
  • 4,326
  • 4
  • 23
  • 46
  • so my recurs function's closure does not have access to the age variable because of where it is defined? – Tamjid Apr 10 '21 at 01:23

7 Answers7

3

I want to make a shell function that sets up the age variable so it doesn't have to be globally declared any time I want to get all the ages. Any idea what is wrong?

You need to declare the recurse function inside getAges, so that it has access to the age variable through closure. Notice that JS only has pass-by-value semantics for function calls, so when you pass a variable (age) as argument to a function, assigning to the parameter (age) will only update the local variable inside the function.

function getAges(profile) {
  let age = 0
  function recurs(profile, age) {
    age += profile.age
    if (!profile.kids) return
    for (const kid of profile.kids)
      recurse(kid)
  }
  recurse(profile)
  return age
}

console.log(getAges(profile))

But as the other answers mentioned, it is much better for recursion to make use of the return value, like in @Nick's or @ippi's snippets.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
1

You can return the age of this person plus all their kids from the recursive function; this can then be summed at the next level up (and so on) to return the total age of the profile:

const profile = {
  name: 'peter',
  age: 56,
  kids: [{
    name: 'jill',
    age: 23,
    kids: [{
      name: 'jeter',
      age: 1
    }, {
      name: 'bill',
      age: 2
    }]
  }]
}

function getAge(profile) {
  let totalage = profile.age;
  if (profile.kids) {
    for (const kid of profile.kids) {
      totalage += getAge(kid);
    }
  }
  return totalage;
}

console.log(getAge(profile));
Nick
  • 138,499
  • 22
  • 57
  • 95
  • thanks for the solution but im still not understanding why the shell function solution doesnt work – Tamjid Apr 10 '21 at 01:21
  • @Tamjid scalar variables in function calls are not passed by reference, so the changes you make inside `recurs` have no effect on the variable in `getAges` – Nick Apr 10 '21 at 01:26
  • @Tamjid and the `age` variable declared in `getAges` is local to that block only, so is only accessible if you define `recurs` inside `getAges` (as described in Bergi answer), which I guess defeats the purpose of the shell function you were trying to create. – Nick Apr 10 '21 at 02:02
1

Firstly, I want to fix your code to make it works.

const profile={name:'peter',age:56,kids:[{name:'jill',age:23,kids:[{name:'jeter',age:1},{name:'bill',age:2}]}]};

function getAges(profile) {
  return recurs(profile)
}

function recurs(profile, age = 0) {
  age += profile.age;
  if(profile.kids){
    for(const kid of profile.kids) {
      age += recurs(kid)
    }
  }
  
  return age;
}
console.log(getAges(profile))

Explanation:

As you can see, the recurs method should return a value. (That's the reason why you get zero value).

Secondly, you can refactor code like this.

const profile={name:'peter',age:56,kids:[{name:'jill',age:23,kids:[{name:'jeter',age:1},{name:'bill',age:2}]}]};

const getAge = (profile) => {
  let totalAge = profile.age;
  if(profile.kids){
    for(const kid of profile.kids){
        totalAge += getAge(kid);
    }
  }
  return totalAge;
}
console.log(getAge(profile));
Nguyễn Văn Phong
  • 13,506
  • 17
  • 39
  • 56
1

I would suggest separating your traverse logic from the sumAges logic. Not only does this simplify the task of each program, but it allows for greater reusability when traversal is needed in other parts of your program -

function* traverse(t)
{ yield t
  if (t.kids)
    for (const k of t.kids)
      yield *traverse(k)
}

function sumAges(t)
{ let sum = 0
  for (const node of traverse(t))
    sum += node.age
  return sum
}

const profile =
  {name: 'peter',age: 56,kids: [{name: 'jill',age: 23,kids:[{name: 'jeter',age: 1}, {name: 'bill',age: 2}]}]}

console.log(sumAges(profile)) // 82

Using the same traverse function we could easily collect all of the names in our tree -

function* traverse(t)
{ yield t
  if (t.kids)
    for (const k of t.kids)
      yield *traverse(k)
}

const profile =
  {name: 'peter',age: 56,kids: [{name: 'jill',age: 23,kids:[{name: 'jeter',age: 1}, {name: 'bill',age: 2}]}]}

const allNames = 
  Array.from(traverse(profile), node => node.name)

console.log(allNames)
[ "peter", "jill", "jeter", "bill" ]

Having traversal separate from makes it easy to write a rich variety of expressions -

function* traverse(t)
{ yield t
  if (t.kids)
    for (const k of t.kids)
      yield *traverse(k)
}

const profile =
  {name: 'peter',age: 56,kids: [{name: 'jill',age: 23,kids:[{name: 'jeter',age: 1}, {name: 'bill',age: 2}]}]}

for (const node of traverse(profile))
  console.log(`I am ${node.name} and I have ${node.kids?.length ?? 0} children`)
I am peter and I have 1 children
I am jill and I have 2 children
I am jeter and I have 0 children
I am bill and I have 0 children
Mulan
  • 129,518
  • 31
  • 228
  • 259
1

This question has already attracted a nice variety of high-quality answers, and others have explained why your attempt fails.

I'd like to offer a variant of what user Thankyou suggested. We can flatten the nested structure into an array of objects, using a preorder traversal. Then if you need to do something involving all the the nodes, you can work with an array, which is often simpler. The code for that is straightforward:

const flatten = (prop) => (o) => 
  [o, ... (o [prop] ?? []) .flatMap (flatten (prop))]

const sum = (ns) => ns.reduce ((a, b) => a + b, 0)

const totalAges = (xs) =>
  sum (flatten ('kids') (xs) .map (p => p.age))

const profile = {name: 'peter', age: 56, kids: [{name: 'jill', age: 23, kids: [{name: 'jeter', age: 1}, {name: 'bill', age: 2}]}]}

console .log (totalAges (profile))

Here flatten takes a string representing the property name of items' children and returns a function that takes an object and traverses it preorder, collecting all the nodes in an array. We call this in totalAges, then pluck off the age properties of each node, finally passing this array to a simple sum function.

This is a simple enough technique, but we might want to make a generic version that passes a function such as node => node.age to directly collect the ages from our traversal. This turns out to be nearly as simple:

const gather = (prop) => (fn) => (o) => 
  [fn (o), ... (o [prop] ?? []) .flatMap (gather (prop) (fn))]

const sum = (ns) => ns.reduce ((a, b) => a + b, 0)

const totalAges = (xs) =>
  sum (gather ('kids') (p => p.age) (xs))

const profile = {name: 'peter', age: 56, kids: [{name: 'jill', age: 23, kids: [{name: 'jeter', age: 1}, {name: 'bill', age: 2}]}]}

console .log (totalAges (profile))

Here totalAges is slightly simpler as we hand off the application of p => p.age to our gather function. But the general logic is much the same.


This technique has one advantage over Thankyou's generator based-technique, and has two disadvantages that I know of. The first disadvantage is that this is subject to recursion depth-limits. Even if we fiddle to make this tail-call optimizable, most engines are still not taking advantage of that, so it could fail on extremely deeply nested object. (This would probably require an object with nodes thousands of levels deep; and if you have such a structure, you probably have other pressing problems too. But it's a valid concern.)

The second disadvantage is that this traversal cannot be cancelled. With a generator-based approach, you can stop early if you decide you're done. Perhaps you want to total the ages only so long as the total does not exceed 1000. That would be trivial in the generator-based approach, but is not really possible here. You could stop summing, but this technique would still traverse the entire tree.

The advantage is one of simplicity. The code to use this is often simpler, partly because there are many more utilities available for working with arrays than for working with iterators. You also get to work with pure expressions, and don't have to fiddle with loop variables and such.

Mind you, this advantage is minor. It's often enough for me to choose this technique over the generator one, but either should work well in practice.

(One minor note: there is no fundamental difference between these two approaches regarding the abstraction of prop and the value of kids. This version could easily be modified to embed kids in the main function, and in fact a partial application will create that, leaving only object -> array. And Thankyou's technique could quite easily be abstracted the way this one is with a prop parameter. That is much more a matter of taste than a fundamental difference.)

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • @Thankyou: you might have some disagreements here. I'd love to hear them, if so. – Scott Sauyet Apr 10 '21 at 23:42
  • 1
    Working with arrays for sure has advantages and generators are markedly slower than an array-based approach. The distinct advantage of generators which tosses any speed metric in the bin is the ability to pause/cancel and you already pointed out. – Mulan Apr 12 '21 at 02:47
  • 1
    One nitpick is to drop the extra parens in the `[o, ...( ___)]` expressions or write this in a way that doesn't need parens at all. Nullish coalescing could be nice here, `[o, ...o[prop]?.flatMap(flatten(prop)) ?? []]`. Or maybe move the conditional outside of the spread, `o[prop] ? [o, ...o[prop].flatMap(flatten(prop)) : [o]`. – Mulan Apr 12 '21 at 02:50
  • 1
    @Thankyou: I haven't done the testing or reading to realize that generators are that much slower. I guess it makes sense. I did miss the extra parentheses -- I use them probably too often to conditionally spread an object inside another, where they're necessary. They weren't here. Someday I'll make the switch to nullish operators, I swear! – Scott Sauyet Apr 12 '21 at 14:15
0

Creating a recursive function, you start with the base case, which is when the recursion should stop. (There may be more than one base case.)

Here it is when when the person in question has no kids.

function getAge(parent){
  // base case
  if (!parent.kids) return parent.age;

  // recursive call for each child
  return parent.kids.reduce((a, child) => a + getAge(child), parent.age);
}

I use a reduce to sum up an array of ints to handle more than one kid. (How to find the sum of an array of numbers)

The default is to not use an accumulator (let totalage = 0) to store the value between recursions. Because your function is not pure if it mutates an outside variable. Sure, you can have a wrapping function which stores the accumulator, and that function is pure. So in the end it's all about how anal you want to be.

ippi
  • 9,857
  • 2
  • 39
  • 50
-1

General Programming languages have Local and global scope global is a scope that is available for every but in case of local it's only accessable within it's block in your code you are making two function first one getAges and second one is recurs age is local variable and only accessable within getAges not in recurs that's why you getting 0 in output. Okay, now move towards Solution one is that you can return a final value from recurs and store it in a variable and return that from getAges . Second one is to make recurs is aynonums function making it aynonums function you can get age directly into recurs due to js lexical environment. I hope this will helpful for you

HASEEB ALI
  • 11
  • 3