DEV Community

David Nguyen
David Nguyen

Posted on

My Adventure with Recursions, Closures, and Callbacks in Javascript

victory
Photo by Nghia Le on Unsplash

Who am I? What's my experience?

Hey all, I'm Dave. A self-taught software engineer, with gaps in knowledge from not knowing what I don't know. Recently I enrolled in a bootcamp, with the aim of improving and solidifying my skills, and filling in those gaps.

What this piece will cover is my solution to a challenge we were given. It is NOT the most efficient solution, and there are bits that may not exhibit best practices. I'm still learning. This here is me aiming to improve my understanding of recursion, closures, and callbacks.

What is the problem I am solving?

My bootcamp provided a challenge where a local marketplace hired us to develop a program. They have a deal where a customer can return their purchased bottles of kombucha (okay I added kombucha, there was no designated drink type) for a free bottle. The deal goes as follows:

  • 2 bottles => 1 free bottle
  • 4 bottle caps => 1 free bottle
  • $2 => 1 bottle

The challenge was to develop a program that will help customers calculate the total amount of bottles they can receive from their initial investment. For example, a $20 investment would net a total of 35 bottles.

Inputs => Outputs

$'s spent Total Bottles of Kombucha
10 15
20 35
30 55
40 75

Final Outputs

Now, it doesn't stop at returning the total number of bottles. After figuring out how to solve that, we are then tasked to print out various information such as remaining number of bottle caps, and how many bottles were earned from returning bottles. Sample output:

Total Bottles:      35
Remaining Bottles:  1
Remaining Caps:     3
Total Earned From:
  Bottles:          37
  Caps:             18

Recursion

recursion-simpsons
Initially I wondered if I needed to use recursion at all...after all, I am not a big fan of recursion. However, it is a concept that I want to be stronger in, so I needed to figure out a way to use recursion to solve this problem.

First, we would solve returning the total number of bottles from a given investment.

let bottlesFromInitialInvestment = invested / 2;
let bottlesEarned = totalEarnedFromBottles(bottlesFromInitialInvestment, bottlesFromInitialInvestment);

totalEarnedFromBottles(bottles,caps) is a separate function where the magic happens. This helper function will calculate how many bottles we earned from a given set of bottles and caps. It takes in number of bottles, and number of caps, and returns the amount earned from those initial values.

Into totalEarnedFromBottles(bottles,caps) we go!

So here is where I wanted to make use of recursion. It is a concept I am still struggling with in terms of real-world usage, and application. However, I understand that at the beginning of any recursive solution...we need to establish the base case. The definition of recursion: "a function that calls itself continuously until it doesn't", the base case helps determine the 'until it doesn't'. This is when the input to our function causes it to stop calling itself.

Before I continue, What is Recursion?

Right. Briefly mentioned before, it is when "a function calls itself until it doesn't". The part about "until it doesn't" is the most important part of recursions, as the absence of an exit, our function will fill up the call stack and cause a stack overflow.

const recurse() => {
  return recurse()
}

recurse()  // Error: Stack Overflow

So we need a way to let the function know to stop calling itself. This is the base case. This can be thought of as the point at which the function can only return one logical answer. Usually this is the simplest scenario, and a good example of how our recursion should function. For example

const sum = (arr) => {
  if (arr.length === 1) return arr[0];

  return arr[0] + sum(arr.slice(1));
}

Here we have a function that sums up the values in an array. Our base case is the if (arr.length === 1) return arr[0] statement. If our sum() function receives an array of one, we want to return that. This is when our function knows to stop calling itself. When it's reached it's end.

Now, how do we bring it to the end in the first place? The trick we went with here was to use Array.prototype.slice(), to remove the first element of the given array, and pass that back into sum(). This is how we recurse sum(). In order to sum all the values, we need to add each value, so the return statement would be the first element of the given array, plus whatever sum() will return from it's next iteration.

sum([1,2,3,4])  // 10

just to break down each iteration, it'd look something like this...

sum([1,2,3,4]) => return 1 + sum([2,3,4])
  sum([2,3,4]) => return 2 + sum([3,4])
    sum([3,4]) => return 3 + sum([4])
      sum([4]) => return 4  // since arr.length === 1, we return arr[0]

    sum([3,4]) = 3 + 4 // 7
  sum([2,3,4]) = 2 + 7 // 9
sum([1,2,3,4]) = 1 + 9 // 10

Anyways, hope that helped. If not, there are many great resources for learning about recursion out there

Back to our problem

I figure, the point to stop recursing is when we do not have enough bottles && caps to earn even 1 bottle, so...

if (bottles < 2 && caps < 4) return 0;

Cool. We got that out of the way.
Next...the recursive case. This is determining how and when we should call our function inside of itself. What is it that we want our function to return each time it's called? That's right, the amount of bottles we can earn from the given number of bottles and caps we received. Well, that's simple enough:

let earnedFromBottles = Math.floor(bottles / 2);
let earnedFromCaps = Math.floor(caps / 4);

let totalEarned = earnedFromBottles + earnedFromCaps;

Not bad at all, we have the amount earned. We can call our function again and give it the amount of bottles we just earned. However, before we do, there's also the matter of how many caps to give, and wait...there may be bottles that weren't used, right? hmmm...So we'll need to calculate the remaining bottles and caps after trading in our bottles and caps, then add that to the next function recursion argument.
Also, let's consider what our function should return. We want it to return the amount earned from given input, right?

...
let remainingBottles = (bottles % 2) + totalEarned;
let remainingCaps = (caps % 4) + totalEarned;

return totalEarned + totalEarnedFromBottles(remainingBottles, remainingCaps);

Phew, looks like we did it. This looks like it should work. So I ran some tests to confirm.
Happily, I am getting back what I should. So we continue forward!
Oh yes, totalEarnedFromBottles() in it's entirety.

const totalEarnedFromBottles = (bottles, caps) => {
  if (bottles < 2 && caps < 4) return 0;

  let earnedFromBottles = Math.floor(bottles / 2);
  let earnedFromCaps = Math.floor(caps / 4);

  let totalEarned = earnedFromBottles + earnedFromCaps;

  let remainingBottles = (bottles % 2) + totalEarned;
  let remainingCaps = (caps % 4) + totalEarned;

  return totalEarned + totalEarnedFromBottles(remainingBottles, remainingCaps);
}

Closures

closure-russian-nesting-doll

Phew! We solved the biggest part of the problem! Right? I mean we mainly needed the net number of bottles from a given investment...right? Well, there's also the matter of how we're going to print the desired data.

We will need to print the number of remaining bottles, number of remaining caps, the number of bottles earned from bottles, and number of bottles earned from caps. That means we'd need to keep track of them somehow, as we recurse through our functions, and return that information back to our main function.

At first, I struggled to find a way to do it. I tried attaching an array to the tail end of totalEarnedFromBottles()'s return object. The idea being I could push the value of 'remaining bottles/caps' each iteration...however, things got messy. Looking back, it was likely due to poor implementation. However...I'm thankful whatever I tried did not work out, as it gave me an opportunity to practice using closures.

Anyways, eventually I remembered that we learned about closures recently, so I read up on it again. The idea that stuck with me about closures was that they can hold a variable, and it's value will not be thrown into the garbage after it's call is over. Now, the actual way it works is a bit more complex than this, but this simplified view made closures accessible for me.

Uh Dave...What's a Closure?

Functions that return a function that has access to the outer scoped function's properties. This inner function is returned or passed to a separate variable or function. This dance enables us to pass around properties, without use of a global property. An example:

const closureCreator = () => {
  let count = 0;

  return () => {
    return count++;
  }
}

let counter = closureCreator();

console.log(counter())  // 0
console.log(counter())  // 1
console.log(counter())  // 2

Pretty cool, eh? So using closures, I figured we could keep track of the data, by calling the closure during each iteration of our recursion.

This was what I came up with:

const closureBottles = () => {
  let earnedFromBottles = [];
  let earnedFromCaps = [];
  let remainingBottles = [];
  let remainingCaps = [];

  return (frBottles, frCaps, remainingBotts, remainingCps) => {
    earnedFromBottles.push(frBottles);
    earnedFromCaps.push(frCaps);
    remainingBottles.push(remainingBotts)
    remainingCaps.push(remainingCps)

    return [earnedFromBottles, earnedFromCaps, remainingBottles, remainingCaps];
  }
}

Initialized a set of arrays for each data piece. Our returning function takes how many bottles we earned from bottles and caps, and the remaining of both. The returning function updates each array with values passed in, then returns the set as an array.

Sweeeet. We got our closure...now how do we use it?

Callbacks

callback-dog

That's right! Callbacks! One of our best buddies in this crazy developer world. We will pass our closure function into our totalEarnedFromBottles(). So we need to modify our totalEarnedFromBottles() to take in a callback as one of it's arguments, then call it with the data we've obtained from each iteration.

Wait, wait, waaaait a second...Dave...what is a callback?

Oh right, in case you're unsure what a callback is, I'll try my best to help you out. Again, I'm simplifying as best as I can, as there are many resources out there with greater finesse in teaching this awesome tool.

A callback is a function which gets passed as an argument to another function (usually something called a 'higher order function'). The higher order function may use this callback to perform certain tasks.

First I initalize our closure in the main function (which is poppinBottles() by the way), then pass that into our call to totalEarnedFromBottles():

  let tracker = closureBottles(0,0);

  let bottlesEarnedTotal = totalEarnedFromBottles([bottlesInitial,0],[bottlesInitial,0], tracker);

Next up, we modify the totalEarnedFromBottles():

const totalEarnedFromBottles = (bottles, caps, callback) => {
  if (bottles[0] < 2 && caps[0] < 4) {
    callback(undefined, undefined, bottles[0], caps[0]);
    return 0;
  } 

  ...

  callback(earnedFromBottles, earnedFromCaps);

  return newBottles 
    + totalEarnedFromBottles([(newBottles + remainingBottles),earnedFromBottles], 
      [(totalCaps),earnedFromCaps], 
      callback);
}

Now every iteration through our recursion, tracker() (masked like a superhero as The callback()) will be called with the amount we earned from bottles and caps, then it will push the new values into each of their respective arrays. We only need to add the remaining amount of each at the end, so we only need to call tracker() when we can't trade for any more bottles. (Found in the if () {} base case)

Back in the main function, we grab all these values from our tracker() - you good boy tracker() 🐕️, you - then print it out for our awesome customer!

let tempArraysOfBottleInfo = tracker().map(arr => arr.filter(e => e !== undefined).reverse()[0]);
  let [ earnedFromBottles, earnedFromCaps, remainingBottles, remainingCaps ] = tempArraysOfBottleInfo;

  let bottlesTotal = bottlesEarnedTotal + bottlesInitial;

  console.log(`
    Total Bottles:      ${bottlesTotal}
    Remaining Bottles:  ${remainingBottles}
    Remaining Caps:     ${remainingCaps}
    Total Earned:
      Bottles:          ${earnedFromBottles}
      Caps:             ${earnedFromCaps}
  `);

The tempArraysOfBottleInfo is grabbing only the values we want from each array. Using map, we iterate through tracker(), clearing out undefined values (as each time we call the function, it will automatically push some value to it's arrays, even this call to grab the values themselves), then from the filtered array, we reverse it, and grab the first item.

After that, we create variables to hold each respective value, and print out the information for our customer. Voila!

Were all of these necessary? What is an alternative way I could've solved the problem?

No. All of these steps were definitely not necessary. You could have made the recursion function take in a single object, and return the same object. With each iteration, you simply update each value. Return that, and doneski!

Anyways, thank you for sticking with me! Appreciate you taking the time. I know I can be a huge scatter-brain, but that's why I'm thankful for software engineering and computer science in general. It helps me be less scattery. That's a topic for another time. For now...here's the code in it's entirety. Have a great one! Sending love, and respect.

let investing = process.argv[2];

const totalEarnedFromBottles = (bottles, caps, callback) => {
  if (bottles[0] < 2 && caps[0] < 4) {
    callback(undefined, undefined, bottles[0], caps[0]);
    return 0;
  } 

  let remainingBottles = bottles[0] % 2;
  let newBottles = Math.floor(Math.floor(bottles[0] / 2) + (caps[0] / 4))
  let totalCaps = (caps[0] % 4) + newBottles;

  let earnedFromBottles = Math.floor(bottles[0] / 2) + bottles[1];
  let earnedFromCaps = Math.floor(caps[0] / 4) + caps[1];

  callback(earnedFromBottles, earnedFromCaps);

  return newBottles 
    + totalEarnedFromBottles([(newBottles + remainingBottles),earnedFromBottles], 
      [(totalCaps),earnedFromCaps], 
      callback);
}

const poppinBottles = (invested) => {
  let bottlesInitial = invested / 2;

  let tracker = closureBottles(0,0);

  let bottlesEarnedTotal = totalEarnedFromBottles([bottlesInitial,0],[bottlesInitial,0], tracker);

  let tempArraysOfBottleInfo = tracker().map(arr => arr.filter(e => e !== undefined).reverse()[0]);
  let [ earnedFromBottles, earnedFromCaps, remainingBottles, remainingCaps ] = tempArraysOfBottleInfo;

  let bottlesTotal = bottlesEarnedTotal + bottlesInitial;

  console.log(`
    Total Bottles:      ${bottlesTotal}
    Remaining Bottles:  ${remainingBottles}
    Remaining Caps:     ${remainingCaps}
    Total Earned:
      Bottles:          ${earnedFromBottles}
      Caps:             ${earnedFromCaps}
  `);

  return bottlesTotal;
}

const closureBottles = () => {
  let earnedFromBottles = [];
  let earnedFromCaps = [];
  let remainingBottles = [];
  let remainingCaps = [];

  return (frBottles, frCaps, remainingBotts, remainingCps) => {
    earnedFromBottles.push(frBottles);
    earnedFromCaps.push(frCaps);
    remainingBottles.push(remainingBotts)
    remainingCaps.push(remainingCps)
    return [earnedFromBottles, earnedFromCaps, remainingBottles, remainingCaps];
  }
}

poppinBottles(investing);

Top comments (0)