Trampoline functions for large recursive loops

Large recursive loops may crash the JavaScript engine as large cycles will continue adding entries into the call stack until the JavaScript engine crashes. Trampoline functions wrap recursive functions in a loop, calling them until they no longer produces recursive calls which prevents the engine from crashing. The following code defines a trampoline function:

const trampoline = (aFunc) => {
   return (...args) => {
      let result = aFunc(...args);
      while(typeof result === 'function')
         result = result();

         return result;
   }
}

Next, the code to create the array that will contain very large amount of random test data:

const dataArray = new Array(999999999);
for(let i = 0; i < dataArray.length; i++)
   dataArray[i] = Math.random().toString(36).substring(2, 3);

Finally, the code that calls the trampoline function that will:

  1. Extract the data from the array.
  2. Add the string || after each character.
  3. Print the result.
const starredString = function(data, myString = '') {
   if(!data.length)
      return myString;

      return () => starredString(data.slice(1), myString.concat(data[0]).concat(' || '));
}

const output = trampoline(starredString);
console.log(`|| ${output(dataArray)}`);

The console output is going to be a very long string similar to the following:

|| b || t || n || x || 0 || b || 4 || y || 9 || ...

Executing code only when needed: lazy evaluation

Lazy evaluation allows programmers to build code that runs only when it is needed which might help optimizing CPU usage. The following code is a function which releases a shippingID when a book is sold in order to prepare the parcel. The function is a generator function which returns objects on which the programmer can call the next() method and it is declared using a star:

const printBook = function*() {
   let shippingID = 0;
   for(; ;) {
      yield shippingID;
      shippingID++;
   }
}

The following code emulates customers who place online orders for books at random times. The snippet is self explanatory as next() is used to retrieve a new shippingID from the generator and return() is used to close the generator:

const pB = printBook();
const options = { hour12: false };
const locale = 'en-AU';
for(let i = 0; i < 10; i++) {
   var t = Math.floor((Math.random() * 1000000000) + 1);
   while (t > 0) { t--; }
   let date = new Date().toLocaleTimeString(locale, options);
   console.log(`Order ${pB.next().value} received at ${date}`);
}
pB.return();

This programming style is only useful for objects with large number of iterations as it does produces complex code which might be difficult to maintain.

Dynamically add properties to objects

Creating objects able to dynamically add properties when needed might help optimizing the memory usage. Moreover, as properties are being added to objects, not classes, recasting an instance will not affect the other ones. The following code creates two basic objects coffee1 and coffee2. Next, two additional properties are added to coffee1 only in order to record the amount of sugar and delivery method requested by the customer. As these properties are not defined for coffee2 too, these values will be undefined for this object:

const coffee1 = { };
const coffee2 = { };
Object.defineProperty(coffee1, 'sugar', {
   value: 2,
   writable: false
});
Object.defineProperty(coffee1, 'takeaway', {
   value: 'yes',
   writable: true
});

coffee1.sugar = 3;
coffee1.takeaway = 'no';

console.log(`[Coffee1] sugar: ${coffee1.sugar} take-away: ${coffee1.takeaway}`);
//This should print 'undefined'
console.log(`[Coffee2] sugar: ${coffee2.sugar} take-away: ${coffee2.takeaway}`);

Although this technology might help in optimizing the memory usage, it also tends to produce code difficult to debug as the design is not shared across all objects.

Previous Post Next Post