Early exits are a coding technique where we return from a function as soon as we encounter a condition that prevents further execution. This approach simplifies code, improves readability, and can enhance performance by avoiding unnecessary computations.
function occurrences(string, substring) {
const lastIndex = string.length - substring.length;
const isSubstringEmpty = !substring;
let subStringCount = 0;
for (let offset = 0; offset <= lastIndex; offset++) {
if (areStringsMatching(string, substring, offset) && !isSubstringEmpty) {
subStringCount++;
}
}
return subStringCount;
}
This looks clean but can be improved significantly. We don't need to run the loop if we know that the substring is empty. We can exit ahead of time.
function occurrences(string, substring) {
if (substring === '') {
return 0;
}
const lastIndex = string.length - substring.length;
let subStringCount = 0;
for (let offset = 0; offset <= lastIndex; offset++) {
if (areStringsMatching(string, substring, offset)) {
subStringCount++;
}
}
return subStringCount;
}
This function exits immediately when it determines that a substring is empty. Additionally, because we've exited, we are no longer unnecessarily checking if the substring is empty within the loop.
Let us consider one more example.
function isPrimeNumber(num) {
if (num < 2) {
return false;
}
for (let divisor = 2; divisor <= num / 2; divisor++) {
if (isDivisible(num, divisor)) {
return false;
}
}
return true;
}
function firstPrimeAbove(number) {
let candidate = number + 1;
while (!isPrimeNumber(candidate)) {
candidate++;
}
return candidate;
}
In this code, we can see that isPrimeNumber
has an early exit. This is pretty good, but this can be improved. This check should happen in firstPrimeAbove
. That way we can ensure that isPrimeNumber
is not called for numbers below 2.
function isPrimeNumber(num) {
for (let divisor = 2; divisor <= num / 2; divisor++) {
if (isDivisible(num, divisor)) {
return false;
}
}
return true;
}
function firstPrimeAbove(number) {
if (number < 2) {
return 2;
}
let candidate = number + 1;
while (!isPrimeNumber(candidate)) {
candidate++;
}
return candidate;
}
The main thing to note her is that this code performs significantly better than the prior one. If firstPrimeAbove
is called with -100 for instance, the older code would have called isPrimeNumber
102 times at least. In the improved version, it returns right away.
function isDivisibleBy3And5(number) {
return number % 3 === 0 && number % 5 === 0;
}
function isDivisibleBy3(number) {
return number % 3 === 0;
}
function isDivisibleBy5(number) {
return number % 5 === 0;
}
The code above seems nice, but has poor abstractions. In this case, there is code that repeats over and over again, but hasn't been extracted into a function. A much better way of writing this would be to have a function called isDivisibleBy
.
function isDivisibleBy(dividend, divisor) {
return dividend % divisor === 0;
}
Consider the temperature conversion example. We have a range of scales to choose from, in this case Centigrade, Fahrenheit and Kelvin. While many of you have several functions that convert from one unit to another, what all of you missed is that the temperature scales are all linear conversions. So, you can write code as follows
function linearScale(slope, intercept, x) {
return slope * x + intercept;
}
function cToF(c) {
return linearScale(9 / 5, 32, c);
}
function cToK(c) {
return linearScale(1, 273.15, c);
}
This abstraction cleans up having to plug in complex formula expressions everywhere. Not only is it more aesthetically pleasing, but it leads to fewer bugs, since the formula has been abstracted away and is only called with various numbers. At this point we can be certain that if there is a bug, it has to do with numbers and not the formula.