Help me make my working "Pig Latin" solution more elegant

Tell us what’s happening:

So I have solved this challenge using a for-loop at first. Then I thuogh, it would be nice to understand recursion better. So I have solved it this way too.

Now I have no issues with how my solution looks like, except for the fact that in the function recursiveAproach I am changing the argument str directly.

This might be just nitpicking, but how would I modify recursiveAproach to work only with the argument passed to it, without modifying str directly?
I presume, that first I would need recursiveAproch to return something back. Most likely in the else statement of recursiveAproach.

  **Your code so far**

function translatePigLatin(str) {
if(str.match(/[aeiou]/i) == null) {
  str = str + "ay"
//No vowels present.
} else if(str[0].match(/[aeiou]/i) !== null) {
  str = str + "way";
//Starts with vowel.
} else {
//For single consonant and consonant clusters.
  const recursiveAproach = (word) => {
    if(str[0].match(/[aeiou]/i) == null) {
      str = (str + str[0]).substring(1);
      recursiveAproach(word);
    } else {
      str = str + "ay";
    }
  }
  recursiveAproach(str);
}
console.log(str); /*console.log used for debuging and troubleshooting.*/
return str;
}

translatePigLatin("schwartz");
//Schwartz used as a good example for a consonant cluster at the beginning of word.

Thank yoou for hints and advice in advance :slight_smile:

  **Your browser information:**

User Agent is: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36.

Challenge: Pig Latin

Link to the challenge:

For what it’s worth, this is a sort of faux recursion. Refactoring this to instead use calls to translatePigLatin and create a return string each time might help you see how to avoid modifying your inputs.

Faux recursion? It is just recursion via an internal helper function. It is still recursion.

I would suggest moving the function out of the else block though.

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.

1 Like

Thank you for such an in-depth explanation!
When it comes to the Pig Latin challenge, it looks to me like recursion is not called for here. A for-loop works perfectly fine and is easier to understand.

The solutions that you have provided are quite a brain teaser and lots of fun :stuck_out_tongue:

Learned something new, thank you for that!

I’m happy to have helped. Trying to articulate what I meant and explain it in the context of this example was super fun for me. I wouldn’t have picked recursion for this problem, so talking about recursion in the context of this problem gave me a chance to think outside of the box and play with ideas in my mind.

Great question!