DEV Community

Cover image for A function by any other name would work as well (part two)
Tracy Gilmore
Tracy Gilmore

Posted on

A function by any other name would work as well (part two)

In part-one of this article we started on our exploration into the extensive world of functions as stipulated by JavaScript, or ECMAScript ECMA-262 to be more exact. We will now be discovering asynchrony and the functional programming side of JS before discussing some of the more exotic forms of function JS has to offer.

Asynchrony

There are actually a number of ways JS supports asynchronous operations, many of which use the Event Loop (see MDN) to control the order in which functions are executed and when.
We will begin by investigating call-backs that have a wide variety of uses. We will then discuss Promises before lastly looking at virtually synchronising operations using async and await instructions.

Call-backs

"Call-back" is this name typically given to a function that is passed to another function as an argument. The intention being that the primary function may, at some point, execute the 'call-back' function. There are many use cases when this mechanism comes in handy but for the majority of us the Event Handler will be our first encounter.

Event handlers

The first event handler a JS developer is likely to encounter is most probably going to be one of the following.

Button Click

Unless the web page you are developing is trying to minimise the use of JavaScript (a noble aim), it is highly likely there will be button of the screen that needs to react (do something) when clicked by the user.

There are several ways to configure this and many JS framework take much of the 'wiring up' out of the developer's hands anyway, but here is one way is can be done under the hood. For our purposes we will contain all of the "moving parts" in the same fragment of an HTML file.

<body>
  <button id="btnClickMe">Click Me</button>

  <script>
    const domButton = document.querySelector('#btnClickMe');
    domButton.addEventListener('click', sayHello);

    function sayHello() {
      alert('Hello there');
    }
  </script>
</body>
Enter fullscreen mode Exit fullscreen mode

The above HTML code is not a complete document but the fragment can be copied to an html file and viewed in a web browser all the same. Web browsers are very forgiving like that and will fill in the blanks.

On screen there will be a single button with the text "Click Me". Such is the nature of HTML buttons that we can either click on the button with the mouse or select it via the keyboard. For the latter we may need to tab round until the button is in focus before pressing the ENTER key to fire the event. Either way the result will be the presentation of an alert box containing the text "Hello there", which will require the OK button to be pressed to cancel.

Page start-up

Another scenario is when we need to execute some JavaScript as soon as the screen had loaded. Again this sort of use case is managed by many JS frameworks as other considerations can impact the user experience.

<script>
    document.addEventListener('DOMContentLoaded', sayHello);

    function sayHello() {
        alert('Hello there');
    }
</script>
Enter fullscreen mode Exit fullscreen mode

In the above example there is even less going on. When the browser completes loading the document any function registered as a listener of the DomContentLoaded event will be executed, which in this case is the same sayHello function we used for the button.

Timer/Interval

There are many use cases for Event Handler call-backs but this third example has to be a very common example encountered by new JS developers.

<script>
    setTimeout(sayHello, 2000);

    function sayHello() {
        alert('Hello there');
    }
</script>
Enter fullscreen mode Exit fullscreen mode

In this example, 2 seconds (or 2000 milli-seconds) after the screen finished loading (and processing the script) the call-back will be executed and the alert banner shown.

This is a textbook example and forms the basis of a very common technical interview question but I have found the need to use setTimeout, or its cousin setInterval are few in production code.

Sort comparators

Probably one of the most common use cases for call-back for a long time was to sort items in an array. The Array object in JS has a number of methods that can perform operations on the content of the array itself, sometimes leaving the updated content in place (as with the sort operation) and otherwise generating a new array, or some other output, as a result.

We will be exploring some of the other methods in a moment but first let's delve a little deeper into the sorting. We will start simply with a list of names.

const names = ['Yvonne', 'Wesley', 'Andrew',
  'Terry', 'Brian', 'Xavier'];

names.sort();

console.table(names);
Enter fullscreen mode Exit fullscreen mode

The above example uses the default simple alphabetical sort comparator function, which using console.table outputs the following.

index Values
0 Andrew
1 Brian
2 Terry
3 Wesley
4 Xavier
5 Yvonne

Obviously the default sort comparator is rather limited. What if we want the names in the reverse order (ok there is a method reverse for that) or the data we want to sort is numeric (we do not want the output like [1, 10, 2]) or a more likely use case, what if the array is of objects rather than primitive values. Then we need to code our own sort comparator.

const objs = [
  { name: 'Yvonne', birthMonth: 8 },
  { name: 'Wesley', birthMonth: 10 },
  { name: 'Andrew', birthMonth: 2 },
  { name: 'Terry', birthMonth: 12 },
  { name: 'Brian', birthMonth: 1 },
  { name: 'Xavier', birthMonth: 4 },
];

objs.sort(sortComparator);

console.table(objs);

function sortComparator(objA, objB) {
  return objA.birthMonth - objB.birthMonth;
}
Enter fullscreen mode Exit fullscreen mode

Observe in the above code example how we have supplied the sort method with a call-back function called sortComparator (but it could be called anything) that expects to be passed two elements from the array and returns a numeric value. In brief, 0 means leave the order unchanged (even if the values are the same), < 0 means arrange in the order objA then objB, > 0 means re-arrange in the order objB then objA (see MDN for more details). With the above comparison of the birthMonth property the output is as follows.

index name birthMonth
0 Brian 1
1 Andrew 2
2 Xavier 4
3 Yvonne 8
4 Wesley 10
5 Terry 12

The sort method removes the need to implement the algorithm in JavaScript and can be implemented as a lower, more performant, level. The frequency in which the for loop is used to traverse the elements of an array is so high it makes sense to also move the code for traversing arrays to a lower, more performant, level; but there are many reasons for traversing an array.

// A common array traversal pattern

const arr = [ ... ];

var index = 0;
for (index = 0; index < arr.length; index++) {

// Code to act on each item of the array (arr[index]) in turn.

}
Enter fullscreen mode Exit fullscreen mode

The above code is not difficult to write or comprehend but so common and easily abstracted.

The next three array methods all use a call-back to produce an output without changing (mutating) the original array. All three methods (along with some others that will be mentioned) are relatively recent additions to the language; added in ECMAScript 6 (2015).

Filter predicates

Using the array of names we used previously, we used to write filter code as follows. In the following example we are filtering (to retain) those names containing the letter 'y'; anywhere in the name and in whatever case.

const names = ['Yvonne', 'Wesley', 'Andrew', 
  'Terry', 'Brian', 'Xavier'];
let namesContainingYs = [];

var index = 0;
for (index = 0; index < names.length; index++) {
  if (names[index].toLowerCase().includes('y')) {
    namesContainingYs.push(names[index]);
  }
}
console.table(namesContainingYs);
Enter fullscreen mode Exit fullscreen mode
index Values
0 Yvonne
1 Wesley
2 Terry

Using the new filter method the code can be simplified to the following.

const names = ['Yvonne', 'Wesley', 'Andrew', 
  'Terry', 'Brian', 'Xavier'];

const namesContainingYs = names.filter(name =>
  name.toLowerCase().includes('y'));

console.table(namesContainingYs);
Enter fullscreen mode Exit fullscreen mode

Key observations include:

  1. The result of the filter is stored in a constant array namesContainingYs.
  2. Filtering is reduced to one line with no need for a for loop or an index variable.
  3. The condition is, of all intents and purposes, unchanged.
  4. The call-back function returns only true or false. This type of function is given the name of predicate and is used by many other array methods such as: every, find, findIndex, findIndexLast, forEach, and some.

It is worth noting that in these examples we will be calling the array methods with one or two arguments but any of them optionally expect more parameters (see MDN for details).

Map transformers

Another common use case for traversing an array is to transform each element from one type to another. For example let's convert all the names to uppercase.

const names = ['Yvonne', 'Wesley', 'Andrew', 
  'Terry', 'Brian', 'Xavier'];

const upperCaseNames = names.map(name => name.toUpperCase());

console.table(upperCaseNames);
Enter fullscreen mode Exit fullscreen mode
index Values
0 YVONNE
1 WESLEY
2 ANDREW
3 TERRY
4 BRIAN
5 XAVIER

Both filter and map traverse the entire array so if your use case requires both (one after the other), where possible, the filter should be performed first to reduce the number of items transformed to a minimum. But we might be able to do better using the next method reduce.

Reduce reducers

The reduce method is more complicated and is a common source of confusion, which is why I have written on the topic. With the reduce method it is possible to implement a version of filter and map without resorting to a for loop; but what is it for?

The filter method will always return an array containing no more items than the source array, and probably less, with the items in the same sequence and unchanged (transformed). The map method will produce a new array containing the same number of items as the source, no more, no fewer.

reduce can do both and more besides. In fact it does not even have to return array but could return another data type such as a count, average or sum. Why we don't have a Math.sum method I do not understand but that is another discussion.

A completely contrived use case but here is how we could use reduce to add-up all the birthMonths.

const objs = [
  { name: 'Yvonne', birthMonth: 8 },
  { name: 'Wesley', birthMonth: 10 },
  { name: 'Andrew', birthMonth: 2 },
  { name: 'Terry', birthMonth: 12 },
  { name: 'Brian', birthMonth: 1 },
  { name: 'Xavier', birthMonth: 4 },
];

const totalBirthMonths = objs.reduce((tot, obj) => 
  tot + obj.birthMonth, 0);

console.log(`Total = ${totalBirthMonths}`); // "Total = 37"
Enter fullscreen mode Exit fullscreen mode

Notice how the running total is initialised as 0 using the second argument of the reduce method. The first argument is called a reducer method because in reduces two input values to one output value. With the reducer the first input is the running total, the second being each item from the array.

As demonstrated above call-back functions are still current and an important technique to understand. However, for async code they can quickly become unmanageable. Before to long there will be a need to nest one call-back inside another. Scaling this pattern is limited as having to many levels of nesting will make the code hard to understand, difficult to debug and virtually impossible to unit test.

Promises

Promises are not functions but a special type of object that is passed a call-back function when created. The call-back function is itself passed two functions as arguments; called resolve and reject.

When a promise starts it executes the initial call-back function to perform its primary purpose. When the function concludes it will call one of the supplied secondary functions depending on if the function was successful (resolve) or failed (reject).

Here is the function that performs the actual coin toss and returns a Promise object for a single coin.

function tossCoin() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const coinToss = ~~(Math.random() * 5);
      if (coinToss) resolve(coinToss % 2 ? 'Tails' : 'Heads');
      reject('Dropped');
    }, 300);
  });
}
Enter fullscreen mode Exit fullscreen mode

Example one (below) uses the 'thenable' protocol implemented by the Promise created above to process the results. Then being called when the coins results in Heads or Tails, catch when the coin is Dropped.

for (let i = 1; i <= 10; i++) {
  tossCoin()
    .then(result => console.log(i, result))
    .catch(result => console.error(i, result));
}
Enter fullscreen mode Exit fullscreen mode

In the above code example we are simulating the tossing of 10 coins. The tossCoin function creates a new Promise, which is the expectation of a coin toss result. However, the person tossing the coins is a bit clumsy and drops the coin 20% (1 in 5) of the time so it is not a 50:50 chance of Head or Tails. The output for the above example, and the next two, look something like this:

1 Heads
2 Tails
3 Dropped
4 Tails
5 Tails
6 Dropped
7 Tails
8 Heads
9 Heads
10 Heads
Enter fullscreen mode Exit fullscreen mode

It takes 300ms for the coin to be flipped and a result to be produced. The conventional for loop calls the function 10 times and about 300ms after (the first call) the results start to flow. Notice how the coin-tosses do not start after the result of the previous coin is known but nearly all at the same time. What this shows is that processing does not stop when the Promise function starts, which gives an effective impression of asynchronous operation, but it is just the Event-loop optimising the primary thread. On the subject of the Event-loop, I highly recommend the article by Lydia Hallie.

Inside the tossCoin function the Math.random method generates a value between 0 and 1. The value is multiplied by 5 and rounded down to an integer between 0 and 4. Each integer representing 20% probability. If the value is zero it is regarded as dropped, otherwise the outcome is Tails if true (odd) or Heads if false (even). When the coin is dropped it is reported by the Promise calling the (exception via the) reject call-back, which exercises the catch path. Valid coin toss results are reported by the Promise calling the resolve call-back via the then path.

Similar to the regular reporting of exceptions, the Promise "thenable" pattern also includes a finally method that is called once the Promise completes irrespective of the outcome as a form of clean-up.

Promises offer a range of supporting methods to help developers write better asynchronous code but reading complex async code some time later can still be difficult. At least, not as easy as reading synchronous code, which is where the following async/await construct comes to the fore.

Async/Await

JavaScript is an unusual language for a number of reasons, not least because it operates in a single thread. But that does not mean it is incapable of supporting asynchronous operations.

After basic call-backs, JS was given Promises but now we also have special (async/await) functions. Under the hood these functions are a combination of a Promise and a Generator (see later) but exactly how is outside the scope of this article.

Async/Await works in conjunction with a Promise but in such as way that the code reads synchronously, which simplifies understanding.

(async function () {
  for (let i = 1; i <= 10; i++) {
    try {
      const result = await tossCoin();
      console.log(i, result);
    } catch (err) {
      console.error(i, err);
    }
  }
})();
Enter fullscreen mode Exit fullscreen mode

Whilst all three examples produce the same output there is a fundamental difference in the behaviour. In the first Promise example there was a 300ms delay before all the results were produced in rapid succession. In the above Async/Await example there is a 300ms delay before the result of the first coin toss is presented before the next 300ms delay starts and so on. This is down to the way the tossCoin function is called inside the for loop with the await keyword.

In the second example all ten coins took around 3 seconds to complete but the first example took a little over 300ms to complete. However, most developers find the syntax of the second example easier on the eye. The slight down-side though is that the await needs to operate inside an async function, which is why the for loop is wrapped in an Immediately Invoked Function Expression (IIFE).

const coins = [];

for (let i = 1; i <= 10; i++) {
  coins.push(tossCoin());
}

Promise.allSettled(coins).then(results =>
  results.forEach(({ status, value, reason }, i) => {
    if (status === 'fulfilled') {
      console.log(i + 1, value);
    } else {
      console.error(i + 1, reason);
    }
  })
);
Enter fullscreen mode Exit fullscreen mode

In the third example, above, the for loop repeatedly calls the tossCoin function and collects the Promises returned in an array. We then use a Promise.allSettled call on the array to process the results when all 10 have completed. The behaviour is like the first example where all 10 coin tosses complete in little over 300ms. The differences include; there is no wrapping async function and no await. The slight complication (there is usually something) is results array is quite involved containing three properties per result:

  • status: The result of the Promise, either "fulfilled" or "rejected".
  • value: If "fulfilled" this is the output of the primary function.
  • reason: If "rejected" this is why the primary function failed.

Functional Programming style

JavaScript is not an FP language but neither is it an entirely OOP language. It is one of a growing number of "multi-paradigm" languages that to some degree supports features from a variety of paradigms, which is probably why it has so many different types of function.

Unlike many of the types of function discussed in this article the following sections describe functions that are not supported directly through syntax in JavaScript; they are not 'idiomatic'. They do however make considerable use of JS's support of functions as first-class objects, high-order functions and closures.

Curried and Partial Application

In my mind Currying is more about the process than a type of function. A 'curried' function is one that expects its parameters to be supplied one argument at a time, returning a new function until all the mandatory parameters have been supplied. Only then is the function actually executed. "Currying" is the process of taking a regular function and generating a curries function.

A textbook example of a curried function could be to calculate the volume of a regular solid shape.

function calculateVolume(width) {
  return function(breadth) {
    return function(height) {
      return width * breadth * height;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

There are four permutations the function can be used but here are the two extremes; all in one or through individual calls.

const allInOne = calculateVolume(2)(3)(7); // 42

const supplyBreadth = calculateVolume(2); // new function
const supplyHeight = supplyBreadth(3); // new function

console.log(supplyHeight(7)); // 42
Enter fullscreen mode Exit fullscreen mode

All of the permutations only accept a single parameter/argument binding with each call but partial-applications are more adaptable. A function to convert a regular function to one that supports partial application is a bit more complicated to implement. Therefor it is recommended to use a library such as Lodash.

The previous example, implemented as a partial-application, can be called in the ways shown above but in addition the following calls are permitted.

const firstTwo = calculateVolume(2, 3);
console.log(firstTwo(7)); // 42

const onlyFirst = calculateVolume(2); // new function
console.log(onlyFirst(3, 7)); // 42
Enter fullscreen mode Exit fullscreen mode

In both of the above examples the first call returns a function expecting more (but not necessarily all of the remaining) arguments.

So what is it good for?
In my related post I discuss this in some detail but a good example is to provide a dynamic property sort comparator. The example source code can be found in JSFiddle.

We start with an array of 18 objects each defining the forename, surname and band name for the members of Fleetwood Mac, Cream, Queen and The Doors. E.g.,

const bandsArray = [
  {
    forename: 'Jack',
    surname: 'Bruce',
    band: 'Cream',
  },

//  :

  {
    forename: 'John',
    surname: 'Deacon',
    band: 'Queen',
  },
];
Enter fullscreen mode Exit fullscreen mode

We can create a function that uses partial application to return the sort comparator function for a given property.

function dynamicSortComparator(propertyName) {
  return (objA, objB) => 
    (objA[propertyName] < objB[propertyName] ? -1 : 1);
}
Enter fullscreen mode Exit fullscreen mode

Conventionally we might call the sort method with a comparator function but that would only be able to compare a specific object property. Instead, using the above function, we can sort by surname or forename as follows:

bandsArray.sort(dynamicSortComparator('surname'));
console.table(bandsArray);

bandsArray.sort(dynamicSortComparator('forename'));
console.table(bandsArray);
Enter fullscreen mode Exit fullscreen mode

I go into more detail of partial applications in this post.

Recursion

Recursion is a technique supported by many programming languages, some better than others. It should be noted that JS is lacking a particular feature called Tail-call optimisation that considerably improves performance of recursive functions but it does not mean it is an inappropriate candidate solution for some problems in JS.

Irrespective of the language or the problem to be solved there are some tips to consider when creating a recursive function. Fundamentally recursive functions call themselves so we need to consider how we stop the function execution going into an infinite loop and never returning.

Textbook examples for demonstrating recursion include calculating factorials or Fibonacci numbers but we will use it to calculate compound interest (interest on interest), to long way.

Mathematicians: I am fully aware there is a convenient formular to do this but that will not demonstrate recursion.

So, the parameters will be:

  • Principal: The amount of the initial loan.
  • Interest rate: Assumed to be constant throughout the period of the loan, and expressed as a percentage, this is the rate the loan will grow (year on year), not considering repayments.
  • Duration: in years the loan is to run.
function calculateInterest(principal, interestRate, duration) {
// recursion happens here
}
Enter fullscreen mode Exit fullscreen mode

Yes, we could use a simple for loop to solve this problem but there are some problems for which for loops are insufficient. Using this problem, which I hope is easy to understand, should be a simple example.

At its core the function we need some simple mathematics to calculate the increase size of the loan for a single year.

  newPrincipal = principal + principal * interestRate; 
Enter fullscreen mode Exit fullscreen mode

But we need to guard against the risk of an infinite loop, which we can do using the duration of the loan.

function calculateInterest(principal, interestRate, duration) {
  if (duration === 0) {  // guard
    return principal;
  }
  return calculateInterest(
    principal + principal * interestRate,
    interestRate,
    --duration  // decrease the duration by 1 year each cycle
  );
}
Enter fullscreen mode Exit fullscreen mode

This would yield the following results:

Principal Year Interest Total Loan
1000 1 200 1200
1200 2 240 1440
1440 3 288 1728
1728 4 345.6 2073.6
2073.6 5 414.72 2488.32

But the function can be simplified into:

function calculateInterest(principal, interestRate, duration) {
  return duration ? calculateInterest(
    principal + principal * interestRate,
    interestRate,
    --duration
  ) : principal;
}
Enter fullscreen mode Exit fullscreen mode

Now for some more exotic functions

A brief "dipping of the toe" into something unfamiliar.

Generators

Arguably, JS Generators support some unusual use cases. If I were more familiar with them, or smarter, I might find more opportunities to use them but I do not think I have ever used them professionally.

In brief, Generators provide a way of creating a 're-enterable' function. The generated function can behave differently each time it is called, re-entering the function at the point it yielded control to the caller.

When trying to explain a complex or novel concept it can be useful to replicate the subject using more familiar techniques. In part-one of this article we discussed closures that we will now use to simulate the behaviour of generators. We will replicate the following example, based on the one from the MDN web page.

{
  const foo = function* () {
    yield 'a';
    yield 'b';
    yield 'c';
  };

  exercise('Idiomatic generator', foo);
}
Enter fullscreen mode Exit fullscreen mode

We will also demonstrate the behaviour and the iteration protocols used through the following exercises.

// Exercise one: Using a for iterator
function exercise(exerciseName, foo) {
  console.log(exerciseName);

  let str = '';
  for (const val of foo()) {
    str += val;
  }
  console.log(str);  // Output: 'abc'
}

// Exercise two: Calling the next method
function exercise(exerciseName, foo) {
  console.log(exerciseName);

  const gen = foo();
  let str = gen.next().value;
  str += gen.next().value;
  str += gen.next().value;
  console.log(str);  // Output: 'abc'
}
Enter fullscreen mode Exit fullscreen mode

The following code fragment is a simulation of the foo generator above but using a closure to maintain state between calls.

{
  const foo = (function (...yieldResults) {
    let yieldIndex = 0;
    return next;

    function next() {
      const mdnIterator = {
        next() {
          return {
            done: yieldIndex === yieldResults.length,
            value: yieldResults[yieldIndex++],
          };
        },
        [Symbol.iterator]() {
          return this;
        },
      };
      return mdnIterator;
    }
  })('a', 'b', 'c');

  exercise('Simulated generator', foo);
}
Enter fullscreen mode Exit fullscreen mode

The simulation is considerably more involved than the MDN example as it has to implement the iteration protocols manually but both implementations perform the exercises to the same effect.

The key points to observe include:

  1. The function is not re-initialised each time it is called.
  2. The yield command behaves a little like a return command and can send a value back from within the function.
  3. Subsequent calls resume from the point of the last used yield.

If you would like read learn more about generators LuisPa García has a post you might find interesting.

AsyncGenerators

Going one step further, there are even asynchronous generators but they are beyond me to explain when they might be of use.

Reader: If you have use cases that demonstrate how these types of function can be used, I would love to learn more. Please write a comment below or better still, write a post and add the link in a comment.

Top comments (1)

Collapse
 
tracygjg profile image
Tracy Gilmore • Edited

For those of you kind enough to have read this post, you might like the first half that can be found here.

For more information of generator functions, here are some article discussing the topic: