Yeah, I meant to say faux. Using an internal helper and relying upon side effects is a way to bypass writing pure functional recursion. It is recursion, yeah, but it increases the size of the code and sort of misses the essence of recursion. That sort of recursion is more of a complex way to write a loop with a helper function and an internal state variable rather than using the function call return values in a functional recursion.
Edit: Let me expand by discussing the classic into to recursion problem, array summing.
// Classic 'for' loop approach
function sumArray(arr, length) {
let sum = 0;
for (let i = 0; i < length; i++) {
sum += arr[i];
}
return sum;
}
In this problem, we need to transform the sumArray
from a for
loop based approach to a recursive approach. (Note that under the covers, high order methods like reduce
and ‘forEach’ are still essentially using this loop based approach)
I will take an intermediate step to rewrite this for
loop as a while
loop.
// Incrementing 'while' loop approach
function sumArray(arr, length) {
let sum = 0;
let i = 0;
while (i < length) {
sum += arr[i];
i++;
}
return sum;
}
or with decrementing
// Decrementing 'while' loop approach
function sumArray(arr, length) {
let sum = 0;
let i = length - 1;
while (i >= 0) {
sum += arr[i];
i--;
}
return sum;
}
If I want to take a ‘helper function recursion’ or ‘faux recursion’ approach, I’d just replace the ‘while’ loop with a helper function.
// Helper function recursion approach
function sumArray(arr, length) {
let sum = 0;
function sumHelper(arr, i) {
if (i < 0) return;
sum += arr[i];
sumHelper(arr, i - 1);
}
sumHelper(arr, length - 1);
return sum;
}
Note that this takes more lines of code, and I even put my if
statement for the base case on a single line. I could have been more compact, but ‘helper function’ or ‘faux’ recursion is just more verbose than ‘functional’ recursion.
In the ‘functional recursion’ approach, I will use the return values of my function calls.
// Functional recursion approach
function sumArray(arr, length) {
if (length < 1) {
return 0;
} else {
return arr[length - 1] + sumArray(arr, length - 1);
}
}
This is much more compact and clear. I could have written this more tersely with a ternary operator, but code golf is not my objective here. I want my code to have fewer pieces so that there is less for me to reason about and less that can break. Simpler syntax is good.
It can be tempting to use the ‘helper function recursion’ or ‘faux recursion’ approach when you first learn about recursion because it is so similar to a while
loop, but it actually adds pieces and complexity to your code.
As an aside, the fact that the classic for
loop approach is simpler is why I tend to use it in production code. Simple code breaks less. Only get fancy when you need to do so for some reason. In that vein, I’d actually do the following
// High order methods approach
function sumArray(arr, length) {
return arr
.slice(0, length) // Only want the first 'length' items
.reduce((sum, item) => sum + item); // Sum items via reduce
}
Personally, I think that recursion is good for learners to practice and understand because it reinforces understanding of key concepts, such as function arguments, variable scope, function call scope, function return values, the call stack, etc. But, I think that recursion should be used extremely sparingly in production code. Recursion is a powerful tool, but there are many times that a recursive approach can be represented more simply and succinctly with a loop based method. There are some specific use cases where recursion really shines, but generally recursion is slower and more complex.
Pulling this all back to the original question, this code is more verbose than it has to be due to the ‘helper function recursion’ or ‘faux recursion’. But we can refactor this as ‘functional recursion’ to clean this up. I will put my version behind double spoiler tags, but first let me give you some hints.
In functional recursion you use the return values and modify the inputs to the recursive call to the function. So I would think about what you want to return and how you can reduce your input for each function call. What is your base case?
Click Here For Spoilers!
// Functional recursion approach
function translatePigLatin(str, firstCall = true) {
const vowelIndex = str
.split("")
.findIndex(letter => "aeiou".includes(letter));
if (vowelIndex === -1) { // Base Case 1
return str + "ay"; // All consonants, like rhythm
} else if (vowelIndex === 0) { // Base Case 2
if (firstCall) {
return str + "way"; // Word begins with vowel, like eight
} else {
return str + "ay"; // Word began with consonant, like glove
}
} else { // Recursive Case
return translatePigLatin(str.slice(1) + str[0], false); // Recursion!
}
}
Now, I can write this a bit more compactly if I combine my cases
// Functional recursion more compactly
function translatePigLatin(str, firstCall = true) {
const vowelIndex = str
.split("")
.findIndex(letter => "aeiou".includes(letter));
if (vowelIndex === -1 || (vowelIndex === 0 && !firstCall)) {
return str + "ay"; // No vowels or vowel rotated to front
} else if (vowelIndex === 0) {
return str + "way"; // Word starts with vowel
} else {
return translatePigLatin(str.slice(1) + str[0], false); // Recursion!
}
}
But this becomes harder to reason about. This is a good example where recursion works but isn’t great.
// Zero recursion approach
function translatePigLatin(str) {
const vowelIndex = str
.split("")
.findIndex(letter => "aeiou".includes(letter));
if (vowelIndex === -1 ) {
return str + "ay"; // No vowels
} else if (vowelIndex === 0) {
return str + "way"; // Word starts with vowel
} else {
// Shift consonant cluster and tack on 'ay' at end
return str.slice(vowelIndex) + str.slice(0, vowelIndex) + "ay";
}
}
If I strip out the recursion, I can simplify the logic, which makes the code easier to maintain.
TLDR: ‘Faux recursion’ with an internal helper function is tempting, but it makes your code more complex. It is basically an obfuscated while
loop that is harder to reason about. If you are going to use recursion you should use ‘functional recursion’. But simpler, more maintainable code is preferable, so you should always use recursion sparingly.