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>
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>
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>
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);
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;
}
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.
}
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);
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);
Key observations include:
- The result of the filter is stored in a constant array
namesContainingYs
. - Filtering is reduced to one line with no need for a
for
loop or anindex
variable. - The condition is, of all intents and purposes, unchanged.
- The call-back function returns only
true
orfalse
. 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);
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"
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);
});
}
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));
}
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
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);
}
}
})();
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);
}
})
);
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;
}
}
}
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
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
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',
},
];
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);
}
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);
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
}
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;
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
);
}
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;
}
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 yield
ed 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);
}
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'
}
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);
}
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:
- The function is not re-initialised each time it is called.
- The
yield
command behaves a little like areturn
command and can send a value back from within the function. - 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)
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: