Logo
Logo

Programming by Stealth

A blog and podcast series by Bart Busschots & Allison Sheridan.

PBS 134 of X — Firming up our Foundations (2 of 2)

In the previous instalment we made a start on firming up some of our foundations in preparation for diving into the Jest testing framework. Between the two instalments we had six topics to cover, four of which were covered in the previous instalment:

  1. Clearing up some confusion around the difference between npm install and npm ci. ✅
  2. Some guidance on which JSDoc tags to use when, especially when documenting plain objects. ✅
  3. A refresher on the different ways of defining functions, specifically function statements, function expressions, and arrow functions. ✅
  4. A reminder on how function chaining works (heavily used by Jest) ✅
  5. An explanation of how getters can be used to construct short but powerful syntaxes that seem quite counterintuitive at first glance (heavily used by Jest)
  6. An introduction to the concept of functions that return functions (used by Jest)

That leaves us with two advanced uses of functions to explore in this instalment.

Matching Podcast Episode

Listen along to this instalment on episode 713 of the Chit Chat Across the Pond Podcast.

You can also Download the MP3

Episode Resources

An Interesting (ab)use of Getters

What I’m going to describe in this section is an interesting combination of two ideas. Both idea are in quite common use, and I’m seeing more and more examples of them being used together, but I can’t seem to find a name for either concept. I tried writing this section without naming the concepts, but I failed miserably, so I’m going to do something I’m very reluctant to do, and coin my own terms — ‘pass-through functions’ & ‘stealth functions’.

‘Pass-Through’ Functions

I want to start with a very quick reminder of what an instance function is, and how they’re used.

When you write a class, any function you add that’s not pre-fixed with the keyword static is, by definition, and instance function. The name implies the way they get invoked — they are called on instances of the class, that is to say, you need an object that is an instance of the class to be able to call the function, and you call the function on that instance using the dot notation:

class DummyClass{
  anInstanceFunction(){
    console.log('blather blather blather');
  }
}

// cannot call an instance function on a class
DummyClass.anInstanceFunction();
// TypeError: DummyClass.anInstanceFunction is not a function

// instance functions are called on instances
const anInstance = new DummyClass();
anInstance.anInstanceFunction(); // works!

Remember that within an instance function, this is a reference to the instance the function was called on, or the thing to the left of the dot operator if you prefer.

Instance functions can return anything, including this. For this instalment, a pass-through function is an instance function that returns this. Remember, this is a term I’m coining because I can’t find a better one, not any kind of widely accepted term!

‘Pass-through’ Functions in Chains

Something interesting happens when you include what I’m calling a pass-through function in a chain — the function executes, but the next function in the chain is called on the same object — the instance to the left of the pass-through function is also the one to the left of the next function in the chain!

We’ve already seen this behaviour — many of jQuery’s instance functions are pass-through functions. Consider the following example:

const $mainHeader = $('<h1>').addClass('mainHeading').attr('id', 'main_heading');

The initial call to the $() function creates a new jQuery object representing a newly created <h1> tag. The next function in the chain is jQuery’s .addClass() instance function, which is a pass-through function that adds a CSS class to the <h1> tag and then returns this. So, the next function in the chain, jQuery’s .attr() instance function is also called on the object created by the $() function, and it add an id attribute to the <h1> tag, and is also a pass-through function, so it also returns the object created by the $() function, which then gets saved to the variable named $mainHeader.

Seeing this concept working in other people’s APIs is one thing, but let’s look at how it’s actually done by creating a simple little class of our own that represents a parrot with one property, a name, a matching getter, and two instance functions that squawk and talk, and return this.

class Parrot{
  constructor(name='Parry'){
    this._name = name;
  }
  get name() { return this._name; }
  squawk(){
    console.log('caaaaw');
    return this;
  }
  talk(){
    console.log(`${this.name} wants a cracker`);
    return this;
  }
}

Because .squawk() & .talk() return this, they act as pass-through functions in function chains:

const p = new Parrot('Polly');
p.squawk().talk().squawk();
// logs:
// ----
// caaaaw
// Polly wants a cracker
// caaaaw

Note: if your JavaScript console also writes returned values to the console, then the final ‘caaaaw’ will be followed by the value returned by the final call to the .squawk() function, i.e. and instance of the Parrot class with the name Polly.

Notice that the object to come out of the end of the chain is the same object that started it, p, the parrot named Polly.

‘Stealth Functions’

We know that getters were added to JavaScript to provide a mechanism for supporting derived attribute values. In the real world, many of a physical object’s external properties can be different aspects of a single true internal property. Think of a Frisbee — it has a clearly measurable diameter, circumference, and area, but while they all have different values, they’re not independent — change one, and they all change. Really, they’re all manifestations of a single true property, the Frisbee’s radius.

If we were to write a class to represent Frisbees it wouldn’t make sense to store the diameter, circumference and area as separate properties, but we do want users of our class to be able to access them all as if they were regular properties. The right way to deal with this in modern JavaScript is to create one regular property to hold the radius, and then use getters to compute the other properties as appropriate. In other words, your code would look something like this:

class Frisbee{
  constructor(r){
    this._radius = r;
  }
  get radius() { return this._radius; }
  get diameter() { return this.radius * 2; }
  get circumference() { return 2 * Math.PI * this.radius; }
  get area() {return Math.PI * this.radius * this.radius; }
}

To a user of the class, the radius looks no different to any of the other properties:

const f = new Frisbee(12)
console.log(`The radius is ${f.radius} inches`);
console.log(`The diameter is ${f.diameter} inches`);
console.log(`The circumference is ${f.circumference} inches`);
console.log(`The area is ${f.area} square inches`);
// Logs:
// -----
// The radius is 12 inches
// The diameter is 24 inches
// The circumference is 75.39822368615503 inches
// The area is 452.3893421169302 square inches

OK, so that’s what getters were designed to be used for, but they’re just functions! Not just any functions though, getters are functions that take no arguments and return a value.

Getters can return any value, even this, so they can be used to create loop-back functions that look like properties!

Let’s update our Parrot class from above to use getters for squawking and talking:

class Parrot{
  constructor(name='Parry'){
    this._name = name;
  }
  get name() { return this._name; }
  get squawk(){
    console.log('caaaaw');
    return this;
  }
  get talk(){
    console.log(`${this.name} wants a cracker`);
    return this;
  }
}

Notice that the only change is the addition of the keyword get twice!

Now that our functions have donned a disguise, we call them as if we were accessing a property, but, they still return this, so they still chain just fine:

const q = new Parrot('yllop'); // Polly sorta-kinda mirrored
q.squawk.talk.squawk
// logs:
// -----
// caaaaw
// yllop wants a cracker
// caaaaw

Note: the NodeJS console’s auto-complete triggers the getters multiple extra times, so this example is best tested in a browser’s JavaScript console.

Because both .squawk and .talk are getters that return this, the code above is equivalent to:

const q = new Parrot('yllop');
q.squawk;
q.talk;
q.squawk;

This is a fun nerd trick, but is it useful?

Using ‘Disguised Loop-back Functions’ for more Human-friendly APIs — A Practical Example

The reason this odd trick is starting to gain popularity is because it can be used to enable more human-friendly code. We’ve already seen this API style in the is.js type checking library where you can do things like:

is.not.string(x);

The .not is a function in disguise!

To see how it’s done, let’s create a simple API of our own, one for joining an array of strings into a human-friendly list.

We’d like to be able to turn an array like ['pancakes', 'waffles', 'popcorn'] into strings like:

In other words, we want to be able to control three things:

  1. The final conjunction (&, and, or or)
  2. Whether or not to sort the list
  3. Which characters the items should be quoted with, if any

The core functionality is quite simple, regardless of the API style we’ll use — there will be a class named Joiner with three properties:

  1. conjunction — the final separator
  2. quoteWith — the character to quote each entry with
  3. doSort— a boolean indicating whether or not to sort the list

We can build the core structure of the class quite easily:

/**
 * A class for joining arrays of strings like a human would.
 */
class Joiner{
    /**
     * @param {string} [conjunction='&'] - the conjunction to use between the last two elements in the array 
     * @param {string} [quoteWith=''] - the character to quote each element in the array with.
     * @param {boolean} [doSort=false] — whether or not to sort the list before joining.
     */
    constructor(conjunction='&', quoteWith='', doSort=false){
        // force all arguments to their appropriate type
        if (typeof conjunction !== 'string') conjunction = '&';
        if (typeof quoteWith !== 'string') quoteWith = '';
        doSort = doSort ? true : false; 

        this._conjunction = conjunction;
        this._quoteWith = quoteWith;
        this._doSort = doSort;
    }

    //
    // Regular getters and setters for both properties
    //

    /**
     * The conjunction to use between the last two array elements.
     * @type {string}
     */
    get conjunction() { return this._conjunction; }
    set conjunction(c){
        this._conjunction = String(c); // force to string
    }

    /**
     * A character with which to quote each array element. Can be an empty string.
     * @type {string}
     */
    get quoteWith() { return this._quoteWith; }
    set quoteWith(qw) {
        if(qw){ // force any truthy value to a string
            this._quoteWith = String(qw);
        }else{
            this._quoteWith = '';
        }
    }

    /**
     * Whether or not to sort the list before joining.
     * @type {boolean}
     */
    get doSort() { return this._doSort; }
    set doSort(s){
        this._doSort = s ? true : false; // force to boolean
    }

    //
    // the actual joiner function
    //

    /**
     * Join an array of strings like a human would list them.
     * @param {Array.<string>} arr - the strings to join.
     * @returns {string}
     * @throws {TypeError} A Type Error is thrown if invalid arguments are passed.
     */
    join(arr){
        // make sure we got an array
        if(!(arr instanceof Array)){
            throw new TypeError('must pass an array');
        }

        // short-circuit an empty array
        if(arr.length === 0) return '';

        // sort the array if needed
        let list = [...arr]; //shallow-clone the array before possibly sorting it
        if(this.doSort) list.sort(); // operates in-place

        // assemble the joined string
        const q = this.quoteWith; // cache the quote character
        let ans = q + String(list[0]) + q; // start with the first element
        for(let i = 1; i < list.length; i++){
            // figure out the separator
            const sep = i === list.length - 1 ? ' ' + this.conjunction + ' ' : ', ';
            ans += sep + q + list[i] + q;
        }
        
        // return the joined string
        return ans;
    }
}

We can now use this class in the traditional way:

const favFoods = ['pancakes', 'waffles', 'popcorn'];
j = new Joiner('&', '', false);
console.log(j.join(favFoods ));
// logs: pancakes, waffles & popcorn
j.doSort = true;
console.log(j.join(favFoods ));
// logs: pancakes, popcorn & waffles
j.conjunction = 'or';
console.log(j.join(favFoods ));
// logs: pancakes, popcorn or waffles
j.quoteWith = '"';
console.log(j.join(favFoods ));
// logs: "pancakes", "popcorn" or "waffles"

Our class works, but the API could be a lot more human-friendly, let’s add some disguised pass-through functions to make using the class both easier and clearer:

class Joiner{
  // …
  
  //
    // The disgusied pass-through functions
    //

    /**
     * A disguised pass-through function that sets the conjuction to 'and'.
     * @type {Joiner}
     */
    get and(){
        this.conjunction = 'and';
        return this;
    }

    /**
     * A disguised pass-through function that sets the conjuction to '&'.
     * @type {Joiner}
     */
    get ampersand(){
        this.conjunction = '&';
        return this;
    }

    /**
     * A disguised pass-through function that sets the conjuction to 'or'.
     * @type {Joiner}
     */
    get or(){
        this.conjunction = 'or';
        return this;
    }

    /**
     * A disguised pass-through function that enables quoting with a single quote.
     * @type {Joiner}
     */
    get quote(){
        this.quoteWith = "'";
        return this;
    }

    /**
     * A disguised pass-through function that enables quoting with a double quote.
     * @type {Joiner}
     */
    get doubleQuote(){
        this.quoteWith = '"';
        return this;
    }

    /**
     * A disguised pass-through function that enables sorting.
     * @type {Joiner}
     */
     get sort(){
        this.doSort = true;
        return this;
    }
}

To make our class even easier to use, let’s also add a so-called factory method to create an instance of our class that can be used as the start of a function chain:

/**
 * A factory method to create instances of the `Joiner` class.
 * 
 * This function takes the same arguments as `Joiner`'s constructor.
 * 
 * @param {string} [conjunction='&'] - the conjunction to use between the last two elements in the array 
 * @param {string} [quoteWith=''] - the character with which to quote each element in the array. 
 * @returns {Joiner} A new Joiner object
 */
function joiner(){
    const ans = new Joiner(...arguments);
    return ans;
}

OK, so now let’s use our new API to replicate the examples from the very start of this section:

const favFoods = ['pancakes', 'waffles', 'popcorn'];
console.log(joiner().ampersand.join(favFoods));
// logs: pancakes, waffles & popcorn
console.log(joiner().quote.sort.and.join(favFoods));
// logs: 'pancakes', 'popcorn' and 'waffles'
console.log(joiner().doubleQuote.or.join(favFoods));
// logs: "pancakes", "waffles" or "popcorn"

The stealthy pass-through functions allow for extremely Englishy syntax!

Note that you’ll find the full code in the instalment ZIP as an ES6 module with one default export, the factory function in joiner.mjs, and the example used above in pbs134a.mjs.

Functions that Return Functions

At this point it seems a good idea to remind ourselves that in JavaScript, everything is either a literal value, or a reference to an object.

Booleans, numbers, and strings are literal values.

Everything else is an object — plain objects are instances of the class Object, arrays are instances of Array, regular expressions are instances of RegExp, exceptions are instances of the class Error or one of its subclasses, and importantly for us today, functions (and classes) are instances of the class Function.

So, a function is an object, and functions take literal values or references to objects as inputs, and can optionally return a single value or reference to an object. We’ve already seen functions being passed to other functions as arguments, and we refer to those as callbacks. What we’ve not looked at yet are functions that return functions.

For completeness, I should also remind you that modern versions of JavaScript automatically convert strings to instances of the String class when needed, hence code like this being possible: console.log("waffles".toUpperCase())

There’s no Special Syntax

We’ve seen how to create anonymous functions, and we’ve seen how to return values from functions, so there’s no new syntax needed, we’ve just never combined these pieces of knowledge in this new way.

As an example, let’s create a function that creates functions that ‘make’ (print) some food:

/**
 * A function to generate functions that 'make' a specific food item.
 * 
 * @param {string} [foodItem='🥞'] - An emoji representing the food to be made by the generated function. Defaults to pancakes.
 * @returns {function} Returns a function that takes one argument, a number, and 'makes' that many items of food by logging the appropriate number of emoji to the console.
 */
function foodMakerMaker(foodItem='🥞'){
  if(typeof foodItem !== 'string') foodItem = '🥞';
  return (n=1) => {
    if(!(String(n).match(/^\d+$/)) && n > 0) n = 1;
    console.log(foodItem.repeat(n));
  };
}

Note that I used an arrow function to define the returned function. For clarity, here’s the returned arrow function on its own:

(n=1) => {
    if(!(String(n).match(/^\d+$/)) && n > 0) n = 1;
    console.log(foodItem.repeat(n));
  };

Notice that this function uses the variable foodItem. Being a named argument, this variable exists in the foodMakerMaker() function’s scope. When we call this function later, will that variable still exist? If we make multiple food makers, will they all share one copy of the foodItem variable? Yes, the variable will continue to exist, and no, it won’t be shared. Why? Because of how JavaScript scopes work.

What happens each time you call the foodMakerMaker() function, is that a new scope is created, and that scope gets a variable named foodItem with the value passed as the first argument. Since nested functions inherit their enclosing function’s scope, that newly created scope is accessible inside the arrow function. When a function ends, the scope is thrown away, but only if nothing else is using it, in this case, the newly created arrow function is, so the scope continues to exist.

The next time the foodMakerMaker() function is called, another new scope is created, complete with its own foodItem, and an entirely new arrow function is created. Additionally, that new arrow function gets to hold onto the scope it was created within, so an entirely different foodItem remains accessible to it forevermore.

This feature of the JavaScript language is referred to as closure.

Let’s prove this works as described:

const pizzaMaker = foodMakerMaker('🍕');
const coffeeMaker = foodMakerMaker('');
pizzaMaker(2);
coffeeMaker();
pizzaMaker();
// logs:
// ----
// 🍕🍕
// ☕
// 🍕

Clearly, the two generated functions don’t share the same foodItem, and clearly both versions continue to exist after the foodMakerMaker() function finishes executing.

In this case we have saved our functions into single variables, so we’ve de-anonymised them. We could, of course, keep them anonymous and save them into an array:

// create arrays of food makers
const starterMakers = [
  foodMakerMaker('🍲'),
  foodMakerMaker( '🥗'),
  foodMakerMaker('🥖')
];
const mainsMakers = [
  foodMakerMaker('🍕'),
  foodMakerMaker('🌮'),
  foodMakerMaker('🍝')
];
const desertMakers = [
  foodMakerMaker('🍰'),
  foodMakerMaker('🥞'),
  foodMakerMaker('🍦')
];

// a function to randomly execute a maker for each course
function randomMeal(){
  starterMakers[Math.floor(Math.random() * 3)]();
  mainsMakers[Math.floor(Math.random() * 3)]();
  desertMakers[Math.floor(Math.random() * 3)]();
}

// call the function
randomMeal();

Notice that in order to execute the function stored in the array, we append parentheses after the square brackets containing the array index. Appending parentheses is how you tell JavaScript to try execute what ever is to the left as a function, and the parentheses contain the argument list, which can be empty.

So, to make three of the first desert we could use:

desertMakers[0](3);

We can, of course, choose not to store functions created by functions at all, and simply execute them immediately by adding an extra set of parentheses after the first:

foodMakerMaker('🥧')(5);
// logs:
// -----
// 🥧🥧🥧🥧🥧

The foodMakerMaker() function creates and returns an anonymous function which then immediately executes with 5 as its first argument.

Final Thoughts

We’ve now seen how it’s possible to create APIs that use what look like properties to create human-readable API calls like joiner().or.sort.join(['a', 'b']). We’ve also learned that functions can return functions, and they can be immediately executed by adding more pairs of parentheses. The reason this was important to do is that our chosen testing framework, Jest, makes heavy use of both concepts in its APIs.

Join the Community

Find us in the PBS channel on the Podfeet Slack.

Podfeet Slack