loading...

Mastering Hard Parts of JavaScript: Closure IV

internettradie profile image Ryan Ameri ・6 min read

Exercise 16

Create a function average that accepts no arguments, and returns a function (that will accept either a number as its lone argument, or no arguments at all). When the returned function is invoked with a number, the output should be average of all the numbers have ever been passed into that function (duplicate numbers count just like any other number). When the returned function is invoked with no arguments, the current average is outputted. If the returned function is invoked with no arguments before any numbers are passed in, then it should return 0.

function average() {}

const avgSoFar = average();
console.log(avgSoFar()); // => should log 0
console.log(avgSoFar(4)); // => should log 4
console.log(avgSoFar(8)); // => should log 6
console.log(avgSoFar()); // => should log 6
console.log(avgSoFar(12)); // => should log 8
console.log(avgSoFar()); // => should log 8

Solution 16

function average() {
  let counter = 0;
  let total = 0;
  function closureFn(num) {
    if (num === undefined) {
      return counter === 0 ? 0 : total / counter;
    }
    counter++;
    total += num;
    return total / counter;
  }
  return closureFn;
}

Again the example output should make the required functionality clear. We are creating averages, so we need two variables in our outside scope, a counter to keep count and a variable to keep track of the total of arguments that have been passed total. Our inside function then exhibits different functionality based on whether it receives an argument or not.

Exercise 17

Create a function makeFuncTester that accepts an array (of two-element sub-arrays), and returns a function (that will accept a callback). The returned function should return true if the first elements (of each sub-array) being passed into the callback all yield the corresponding second elements (of the same sub-array). Otherwise, the returned function should return false.

function makeFuncTester() {}
const capLastTestCases = [];
capLastTestCases.push(["hello", "hellO"]);
capLastTestCases.push(["goodbye", "goodbyE"]);
capLastTestCases.push(["howdy", "howdY"]);
const shouldCapitalizeLast = makeFuncTester(capLastTestCases);
const capLastAttempt1 = (str) => str.toUpperCase();
const capLastAttempt2 = (str) => str.slice(0, -1) + str.slice(-1).toUpperCase();
console.log(shouldCapitalizeLast(capLastAttempt1));
// => should log false
console.log(shouldCapitalizeLast(capLastAttempt2));
// => should log true

Solution 17

function makeFuncTester(arrOfTests) {
  function closureFn(callback) {
    return arrOfTests.every((couple) => callback(couple[0]) === couple[1]);
  }
  return closureFn;
}

We are mixing closure and callbacks, so it can be a little confusing here, but basically passing an array (of arrays) to our outer function, and then when we provide a callback as an argument to the inside function, we want to make sure that the result of the callback is correctly stored as the second element in our original array.

Notice the use of Array.prototype.every() method here, a very useful Array method that returns true only if the callback returns true for every element of the array. It simplifies our code quite a bit.

Exercise 18

Create a function makeHistory that accepts a number (which will serve as a limit), and returns a function (that will accept a string). The returned function will save a history of the most recent "limit" number of strings passed into the returned function (one per invocation only). Every time a string is passed into the function, the function should return that same string with the word 'done' after it (separated by a space). However, if the string 'undo' is passed into the function, then the function should delete the last action saved in the history, and return that deleted string with the word 'undone' after (separated by a space). If 'undo' is passed into the function and the function's history is empty, then the function should return the string 'nothing to undo'.

function makeHistory() {}

const myActions = makeHistory(2);
console.log(myActions("jump"));
// => should log 'jump done'
console.log(myActions("undo"));
// => should log 'jump undone'
console.log(myActions("walk"));
// => should log 'walk done'
console.log(myActions("code"));
// => should log 'code done'
console.log(myActions("pose"));
// => should log 'pose done'
console.log(myActions("undo"));
// => should log 'pose undone'
console.log(myActions("undo"));
// => should log 'code undone'
console.log(myActions("undo"));
// => should log 'nothing to undo'

Solution 18

function makeHistory(limit) {
  const memory = [];
  function closureFn(input) {
    if (input !== "undo") {
      if (memory.length >= limit) memory.shift();
      memory.push(input);
      return input + " done";
    } else {
      if (memory.length === 0) return "nothing to do";
      let remove = memory.pop();
      return remove + " undone";
    }
  }
  return closureFn;
}

Implementing "undo" was an interesting challenge. Turns out we basically need our usual memory in the outer scope (this time in the form of an array) but our memory should only stretch limit items. So we need to keep count of how many items is in our memory array, and if we input more elements in it, implement a sliding window as in a FIFO way to only keep the correct number of items in it.

Exercise 19

Inspect the commented out test cases carefully if you need help to understand these instructions.

Create a function blackjack that accepts an array (which will contain numbers ranging from 1 through 11), and returns a DEALER function. The DEALER function will take two arguments (both numbers), and then return yet ANOTHER function, which we will call the PLAYER function.
On the FIRST invocation of the PLAYER function, it will return the sum of the two numbers passed into the DEALER function.

On the SECOND invocation of the PLAYER function, it will return either:

the first number in the array that was passed into blackjack PLUS the sum of the two numbers passed in as arguments into the DEALER function, IF that sum is 21 or below, OR
the string 'bust' if that sum is over 21.

If it is 'bust', then every invocation of the PLAYER function AFTER THAT will return the string 'you are done!' (but unlike 'bust', the 'you are done!' output will NOT use a number in the array). If it is NOT 'bust', then the next invocation of the PLAYER function will return either:

the most recent sum plus the next number in the array (a new sum) if that new sum is 21 or less, OR
the string 'bust' if the new sum is over 21.

And again, if it is 'bust', then every subsequent invocation of the PLAYER function will return the string 'you are done!'. Otherwise, it can continue on to give the next sum with the next number in the array, and so forth.
You may assume that the given array is long enough to give a 'bust' before running out of numbers.

BONUS: Implement blackjack so the DEALER function can return more PLAYER functions that will each continue to take the next number in the array after the previous PLAYER function left off. You will just need to make sure the array has enough numbers for all the PLAYER functions.

function blackjack() {}
// /*** DEALER ***/
const deal = blackjack([
  2,
  6,
  1,
  7,
  11,
  4,
  6,
  3,
  9,
  8,
  9,
  3,
  10,
  4,
  5,
  3,
  7,
  4,
  9,
  6,
  10,
  11,
]);

// /*** PLAYER 1 ***/
const i_like_to_live_dangerously = deal(4, 5);
console.log(i_like_to_live_dangerously());
// => should log 9
console.log(i_like_to_live_dangerously());
// => should log 11
console.log(i_like_to_live_dangerously());
// => should log 17
console.log(i_like_to_live_dangerously());
// => should log 18
console.log(i_like_to_live_dangerously());
// => should log 'bust'
console.log(i_like_to_live_dangerously());
// => should log 'you are done!'
console.log(i_like_to_live_dangerously());
// => should log 'you are done!'

// /*** BELOW LINES ARE FOR THE BONUS ***/

// /*** PLAYER 2 ***/
const i_TOO_like_to_live_dangerously = deal(2, 2);
console.log(i_TOO_like_to_live_dangerously());
// => should log 4
console.log(i_TOO_like_to_live_dangerously());
// => should log 15
console.log(i_TOO_like_to_live_dangerously());
// => should log 19
console.log(i_TOO_like_to_live_dangerously());
// => should log 'bust'
console.log(i_TOO_like_to_live_dangerously());
// => should log 'you are done!
console.log(i_TOO_like_to_live_dangerously());
// => should log 'you are done!

// /*** PLAYER 3 ***/
const i_ALSO_like_to_live_dangerously = deal(3, 7);
console.log(i_ALSO_like_to_live_dangerously());
// => should log 10
console.log(i_ALSO_like_to_live_dangerously());
// => should log 13
console.log(i_ALSO_like_to_live_dangerously());
// => should log 'bust'
console.log(i_ALSO_like_to_live_dangerously());
// => should log 'you are done!
console.log(i_ALSO_like_to_live_dangerously());
// => should log 'you are done!

Solution 19

function blackjack(array) {
  let dealerCount = 0;
  function dealer(a, b) {
    let playerCount = 0;
    let total = a + b;
    function player() {
      if (total === "bust") return "you are done!";
      dealerCount++;
      playerCount++;
      if (playerCount === 1) return total;
      total += array[dealerCount - 2];
      if (total > 21) {
        total = "bust";
        dealerCount--;
      }
      return total;
    }
    return player;
  }
  return dealer;
}

By this point, the code should be pretty self-explanatory so I won't explain it line by line. The most important concept here is that we have two closures here, one inside the other. The outer function can be thought of as the deck of cards, the function inside that can be thought of as the dealer, and the one inside that can be thought of as the players. Thinking logically about blackjack, a dealer can deal many players, and a single deck of cards can be used in many dealings. Thinking like this should clarify where each variable that acts as memory should reside.

Implementing the bonus part just required realising that we needed two different counters, one for the dealer and one for the players, and then to modify the logic very slightly to count correctly.

I know I've harped on this time and time, but I have implemented blackjack exercises quite a few times in different languages, generally using OOP paradigms. It has always required a lot more code than this. Using closure and realising the power that having memory gives functions is quite amazing.

We're done with closure exercises. Next up: Asynchronous JavaScript!

Posted on by:

internettradie profile

Ryan Ameri

@internettradie

Re-discovering frontend development. Professional translator & interpreter. Amateur powerlifter. BLM. He/him/his

Discussion

pic
Editor guide