Goose9192
Goose9192

Reputation: 107

Converting a loop function into a recursive function in javascript

I am attempting to teach myself how to write recursive functions and someone suggested trying to convert loops into recursions. So I am trying to change the first for loop function into a recursive function. Here is my code:

// Function that uses for loop.
function onlyOne(value1, value2, value3) {
    var array = [value1, value2, value3];
    var count = 0;
    for(var i = 0; i < array.length; i++) {
        if(!!array[i] === true) {
            count ++;
        }
    } if(count === 1) {
        return true;
    } else {
        return false;
    }
}

// Function that uses recursion.
function onlyOne2(a, b, c) {
    var array = [a, b, c];
    var count = 0;
    var numTrue = 0;
    if(!!array[count] === true) {
        numTrue++;
    }
    if(count === array.length-1) {
        if(numTrue === 1) {
            return true;
        } else {
            return false;
        }
    }else {
        count++;
        return onlyOne2(a, b, c);
    }
}

console.log(onlyOne2(true, false, false));

The purpose of each function is to return true if there is exactly one argument that is truthy. Otherwise the function returns false. The for loop function works properly. However, when I use the recursive function I get the error: Maximum call stack size exceeded. I was wondering what I am doing incorrectly. Thank you for your help!

Upvotes: 3

Views: 4355

Answers (9)

Aadit M Shah
Aadit M Shah

Reputation: 74204

You're going about this the wrong way. You're trying to convert the entire function into a recursive function. Instead, as suggested, you only need to convert the loop into a recursive function:

I am attempting to teach myself how to write recursive functions and someone suggested trying to convert loops into recursions.

So, let's start by looking at your original function. I cleaned it up for you:

assert("Test 1", onlyOne(false, 0, "")  === false);
assert("Test 2", onlyOne(false, 0, "x") === true);
assert("Test 3", onlyOne(false, 1, "")  === true);
assert("Test 4", onlyOne(false, 1, "x") === false);
assert("Test 5", onlyOne(true,  0, "")  === true);
assert("Test 6", onlyOne(true,  0, "x") === false);
assert("Test 7", onlyOne(true,  1, "")  === false);
assert("Test 8", onlyOne(true,  1, "x") === false);

function onlyOne(a, b, c) {
    const array = [a, b, c];

    //    +-- state of the loop
    //    |     +-- initial value of the state
    //    |     |
    //    v     v
    var count = 0;

    //       +-- loop variant
    //       |   +-- initial value of the loop variant
    //       |   |
    //       v   v
    for (let i = 0; i < 3; i++)
        if (array[i]) count++;

    return count === 1;
}

function assert(test, condition) {
    console.log(test, condition ? "passed" : "failed");
}

Notice that I annotated the state of the loop and the loop variant. These will be the arguments of our recursive function. We'll replace the for loop with a call to the recursive function using the initial values of the state and the loop variant as inputs. The result of the recursive function will be the final state of the loop:

assert("Test 1", onlyOne(false, 0, "")  === false);
assert("Test 2", onlyOne(false, 0, "x") === true);
assert("Test 3", onlyOne(false, 1, "")  === true);
assert("Test 4", onlyOne(false, 1, "x") === false);
assert("Test 5", onlyOne(true,  0, "")  === true);
assert("Test 6", onlyOne(true,  0, "x") === false);
assert("Test 7", onlyOne(true,  1, "")  === false);
assert("Test 8", onlyOne(true,  1, "x") === false);

function onlyOne(a, b, c) {
    const array = [a, b, c];

    const count = loop(0, 0);

    return count === 1;

    function loop(count, i) {
        return i < 3 ?
            loop(array[i] ? count + 1 : count, i + 1) :
            count; // return final state of the loop
    }
}

function assert(test, condition) {
    console.log(test, condition ? "passed" : "failed");
}

Hopefully, now you know how to convert any loop into a recursive function.

Upvotes: 0

Nina Scholz
Nina Scholz

Reputation: 386560

You could take a counter for the wanted truthy values and rest parameters ... for the recursive function.

It works with tail recursion and an arbitrary count of arguments.

function only(count, v, ...rest) {
    if (v) {                       // check value
        if (!count) {              // check count is zero
            return false;          // because found more truthy than needed
        }
        count--;                   // decrement count
    }
    if (!rest.length) {            // check length is zero
        return !count;             // return not count
    }
    return only(count, ...rest);   // tail call with count and rest
}

console.log(only(1, false, false, false)); // false
console.log(only(1, false, false, true));  //  true
console.log(only(1, false, true, false));  //  true
console.log(only(1, false, true, true));   // false
console.log(only(1, true, false, false));  //  true
console.log(only(1, true, false, true));   // false
console.log(only(1, true, true, false));   // false
console.log(only(1, true, true, true));    // false
console.log('-----');
console.log(only(2, false, false, false)); // false
console.log(only(2, false, false, true));  // false
console.log(only(2, false, true, false));  // false
console.log(only(2, false, true, true));   //  true
console.log(only(2, true, false, false));  // false
console.log(only(2, true, false, true));   //  true
console.log(only(2, true, true, false));   //  true
console.log(only(2, true, true, true));    // false
.as-console-wrapper { max-height: 100% !important; top: 0; }

Upvotes: 1

Mulan
Mulan

Reputation: 135197

Here's another way to solve it – I recently wrote another answer that aims to help learners achieve high-level thinking using recursion. Techniques used here are written about in detail. If you're interested, you can read it here.

const None =
  Symbol ()

const loop = ([ x = None, ...xs ], foundTruthy = false) =>
  x === None
    ? foundTruthy
    : Boolean (x)
      ? foundTruthy
        ? false
        : loop (xs, true)
      : loop (xs, foundTruthy)

const onlyOne = (...xs) =>
  loop (xs, false)

console.log
  ( onlyOne ()                 // false
  , onlyOne (0)                // false
  , onlyOne (1)                // true
  , onlyOne (0, '', false, 1)  // true
  , onlyOne (0, 0, 0, true, 1) // false
  )

Note loop can be made generic so that onlyOne doesn't need to depend on a specialized helper – mind the stack overflow

const None =
  Symbol ()

const always = x => _ =>
  x

const recur = (...values) =>
  ({ recur, values })

const loop = (f = always (null) , acc = f ()) =>
  acc && acc.recur === recur
    ? loop (f, f (...acc.values))
    : acc

const onlyOne = (...list) =>
  loop (([ x = None, ...xs ] = list, foundTruthy = false) =>
    x === None
      ? foundTruthy
      : Boolean (x)
        ? foundTruthy
          ? false
          : recur (xs, true)
        : recur (xs, foundTruthy))

console.log
  ( onlyOne ()                 // false
  , onlyOne (0)                // false
  , onlyOne (1)                // true
  , onlyOne (0, '', false, 1)  // true
  , onlyOne (0, 0, 0, true, 1) // false
  )

Nina's answer is a great example of how function parameters can reduce program complexity – this adaptation shows how the same program can be written in a functional expression

const None =
  Symbol ()

const only = (count = 1, x = None, ...xs) =>
  x === None
    ? count === 0
    : Boolean (x)
      ? only (count - 1, ...xs)
      : only (count, ...xs)
      
console.log
  ( only ()                           // false
  , '---'
  , only (0)                          // true
  , only (0, false)                   // true
  , only (0, true)                    // false
  , '---'
  , only (1)                          // false
  , only (1, false)                   // false
  , only (1, false, true)             // true
  , only (1, false, true, true)       // false
  , '---'
  , only (2, false, true)             // false
  , only (2, false, true, true)       // true
  , only (2, false, true, true, true) // false
  )

Upvotes: 0

DJDaveMark
DJDaveMark

Reputation: 2855

I would recommend starting with a simpler example:

function logRecurse(i, arr) {
    if (i < arr.length) {
        console.log(arr[i]);
        logRecurse(i+1, arr);
    }
}

var array = [false, false, true, false];
logRecurse(0, array);

Then add the return statements:

function truthyRecurse(i, arr) {
    if (i < arr.length) {
         if (arr[i]) { // could be just: return arr[i] || truthyRecurse(...)
            return true;
         } else {
            return truthyRecurse(i+1, arr);
         }
    }
    return false;
}

var array = [false, false, true, false];
console.log(truthyRecurse(0, array));

But the real usefulness of recursion is with something like a directory structure. Have a go at that next! You can put arrays into arrays to simulate it:

function logFiles(dir) {
    // log filename (typeof string) or recurse into dir
}

var dir = [
    "file1",
    "file2",
    [ // sub dir
        "subfile1",
        [ // sub-sub dir
            "sub-subfile1",
            "sub-subfile2",
            "sub-subfile3",
        ],
        "subfile2",
    ]
];

logFiles(dir);

Upvotes: 0

גלעד ברקן
גלעד ברקן

Reputation: 23955

This is interesting. Ok, so we have a function that returns true if exactly one of the items in the array is truthy. Let's think about the basic requirements in a recursion.

Base case, array is empty:

if (!array.length)
  return false;

Base case, there's only one element in the array:

if (array.length == 1)
  return !!array[0] === true

Otherwise:

let next = f(rest-of-array)

if (!!array[0] === true)
  return !next
else
  return next

JavaScript code:

function f(arr, i){
  // Same as !array.length
  if (i == arr.length)
    return false;

  // Same as array.length == 1
  if (i == arr.length - 1)
    return !!arr[i]

  // Same as f(rest-of-array)
  let next = f(arr, i + 1)

  if (!!arr[i])
    return !next

  return next
}

Output:

   f([undefined,2,0], 0)
=> true
   f([1,2,undefined], 0)
=> false
   f([undefined,false,1], 0)
=> true
   f([5,false,false], 0)
=> true

Upvotes: 0

Wdy Dev
Wdy Dev

Reputation: 229

You might as well try this. You dont need to change your function structure or anything and it will work with any number of arguments.

function onlyOne(a,b,c){
 let arr = Array.from(arguments);
 let count = 0;
 let item = arr.shift();
 if(true === item){
    ++count;
 }
 if(arr.length > 0) {
    count += onlyOne.apply(this,arr);
    return count == 1;
 }
 return count;
}

Upvotes: 0

Sylwester
Sylwester

Reputation: 48745

You can convert any for loop to recursion:

let a=0, b=1,
for (let n=5; n > 0; n--) {
  const tmpa = a;
  a = b; 
  b += tmpa;
}

Here a. b and n changes and the reason is the a value which has the 6th fibonacci number. So we make them parameters and return:

function forLoop(n, a, b) {
  if(n > 0) {
    return forLoop(n - 1, b, a + b);
  }
  return a;
}
const a = forLoop(5, 0, 1);

So for your function that uses for loop:

function onlyOne(value1, value2, value3) {
  const array = [value1, value2, value3];
  let count = 0;
  for(var i = 0; i < array.length; i++) {
    if(!!array[i] === true) {
      count ++;
    }
  } 
  if(count === 1) {
    return true;
  }
  return false;
}

The bindings you are changing are count and i:

function onlyOne(value1, value2, value3) {
  function iterate(count, i) {
    if (i < array.length) {
      if (!!array[i] === true) {
        return iterate(count + 1, i + 1);
      }
      return iterate(count, i + 1);
    }
    return count;
  }
  const array = [value1, value2, value3];
  const count = iterate(0, 0);
  if(count === 1) {
    return true;
  }
  return false;
}

Since you only have 3 elements this probably is good enough but imagine you wanted to implement something like this for an array you should plan for returning as fast as you know you are done. In a forversion it would look like this:

function onlyOne(arr) {
  let count = 0;
  const len = array.length; // cache
  for (index = 0; index < len; index++) {
    if (arr[index]) {
      count++; 
      if(count > 1) return false; // early return
    }
  }
  return count === 1;
}

The same with recursion:

function onlyOne(arr) {
  function iterate (index, count) {
    if (index < len && count <= 1) {
      return iterate(
        index + 1, 
        count + (array[index] ? 1 : 0)
      );
    }
    return count;
  }
  const len = array.length; // cache
  const count = iterate(0, 0);
  return count === 1;
}

Notice it's not exactly the same since we are expecting a count from the iteration and return only returns back to the callee. Thus early return here is just not iterating more values but we still return a count that is the subject of the final result. You could do the same in the for by using break instead of return.

The same goes for any/some which should return at the very first true and all that should return false for the first false value. No point in iterating through every element when it cannot change the answer.

Upvotes: 0

Ele
Ele

Reputation: 33726

Your approach has some issues

  • if(!!array[i] === true) use the boolean value directly: if(array[i])

You're asking for a true value to return true

if (count === 1) { // <- Use this comparison to return the specific value.
    return true;
    ^
} else {
    return false;
    ^
}

Return the comparison directly: return count === 1;

In your function onlyOne2 your "recursion case" to stop the loop is incorrect.

 count === array.length-1
 ^         ^ 
   

You have to use the index i

Look at this code snippet with those fixes

// Function that uses for loop.
function onlyOne(value1, value2, value3) {
  var array = [value1, value2, value3];
  var count = 0;
  for (var i = 0; i < array.length; i++) {
    if (array[i]) {
      count++;
    }
  }

  return count === 1;
}

console.log(onlyOne(true, false, false));

function onlyOne2(value1, value2, value3, count, i) {
  var array = [value1, value2, value3];

  if (i === array.length) return count === 1;
  
  if (array[i]) count++;

  return onlyOne2(value1, value2, value3, count, ++i);
}

console.log(onlyOne2(true, false, false, 0, 0));

See? now is working the loop using recursion.

Upvotes: 1

Shubham
Shubham

Reputation: 168

The problem with above code is that every time the function recurses count and numTrue get declared again and hence an infinite loop occurs, Here is the correct code(declaring count and numTrue as global:

var count = 0;
var numTrue = 0;

function onlyOne2(a, b, c) {
    var array = [a, b, c];
    if(!!array[count] === true) {
        numTrue++;
    }
    if(count === array.length-1) {
        if(numTrue === 1) {
            return true;
        } else {
            return false;
        }
    }else {
        count++;
        return onlyOne2(a, b, c);
    }
}

console.log(onlyOne2(true, false, false));

Upvotes: 0

Related Questions