Thinking Recursively
Thinking recursively doesn’t come naturally to me so I am fascinated by the fact that iterative functions can be expressed recursively. In this post I offer a step-by-step demonstration of how to turn the iterative Rock Paper Scissors solution into the recursive one.
Iteration or Recursion?
Besides just the pleasure of getting a better understanding of recursion, why would I want to turn an iterative function into a recursive one?
-
Flexibility: We use recursion when we have a complex task that can be broken up into many similar subtasks. In the Rock Paper Scissors example, if we want the user to determine how many rounds of play will occur then we don’t know in advance the number of iterations we’ll need. Recursion allows the flexibility to continue working on these subtasks for any number of iterations. We just need to know when to stop / what the exit strategy is.
-
Mutation: Recursion allows for iteration without mutation. Mutating internal variables is usually harmless, but sometimes data mutation can cause unintended side effects. (For more information, check out the wikipedia article on Pure functions.) In the Rock Paper Scissors example, we use .concat to create a new array with the additional element without modifying or destroying the original array.
-
Expressiveness and elegance: Recursive functions tend to be beautifully succint and it can be incredibly satisfying to write such elegant code. This isn’t always the case, however. Some algorithms naturally lend themselves better to one approach or another. For more complex systems, we often spend more time testing and debugging code than we do writing it so it’s important to think of the readability of your code as well as the ease of maintenance.
But there are alos some reasons why I might not want a recursive solution. A recursive function calls itself to complete each of its subtasks. Each of these function calls gets put on the function call stack until that function is completed.
-
Performance: In general, recursion tends to run more slowly than iteration because there is the overhead cost of multiple function calls.
-
Stack space: Each of these function calls gets added to the stack and stack space is limited. This can eventually cause a stack overflow.
Of course, there are ways to minimize performance and stack space issues, but that’s not the focus of this post.
Step-By-Step from the Iterative Solution:
Let’s start with the case that there are three rounds of play and therefore three nested for loops:
Step One: Start at the Inner-Most For-Loop and Create a New Function
In this example, we’ll want to refactor the line of code that pushes the possible outcomes (
) into the 1
[plays[k], plays[j], plays[i]]
array as a separate and stand-alone function.1
outcomes
Let’s call this new function
(we’ll be making a number of these 1
combos_0
functions). Since 1
combos
is going to push an array into 1
combos_0
, 1
outcomes
needs to take an array as an argument. Let’s call this 1
combos_0
since it will collect each of three plays in the three rounds.1
playedSoFar
This function above will be invoked when we’re done with the recursion.
Step Two: Go Loop By Loop and Create New Functions
We’ll start at the deepest of these loops and then work our way up. This gives us three different functions, one for each of the nested for loops. At each stage, we concatenate the play at each of the indices of the
array into the 1
plays
array.1
playedSoFar
It’s clear from the three functions above that there is a pattern here that is particularly good for recursion - the work can be divided into smaller subtasks that all do the same thing. We can combine the three functions
, 1
combos_1
, and 1
combos_2
into one function that is invoked when with the recursive call.1
combos_3
Step Three: Putting the Recursive Subroutine Together
Now we need to figure out how the parts from Step One and Step Two fit together in a recursive subroutine. We’ll call this subroutine
.1
getOutcomes
Combining the 1
combos_n
functions
1 | combos_n |
We’ll use the for loop found in the
functions to iterative over the plays list but now we’ll be completing the subtasks by calling the recursive subroutine.1
combos
But what is the argument that goes into this subroutine? How about the following?
We actually want to make calls to the recursive function without mutating the original data. We shouldn’t use
to modify 1
push
because it will mutate the array. Instead, we need to use 1
playedSoFar
which creates a new array and is therefore not destructive.1
concat
An example:
-
Imagine
contains [‘rock’]1
playedSoFar
-
We want to iterate through the for loop so that each iteration adds one more possible play onto
so that we get1
playedSoFar
([‘rock’]) + ‘rock’ ([‘rock’, ‘rock’]),1
playedSoFar
([‘rock’]) + ‘paper’ ([‘rock’, ‘paper’]), and1
playedSoFar
([‘rock’]) + ‘scissors’ ([‘rock’, ‘scissors’]).1
playedSoFar
-
DON’T USE PUSH: If we push each of these plays into
, we’ll get1
playedSoFar
([‘rock’]) + ‘rock’ ([‘rock’, ‘rock’]),1
playedSoFar
([‘rock’, ‘rock’]) + ‘paper’ ([‘rock’, ‘rock’, paper’]), and1
playedSoFar
([‘rock’, ‘rock’, paper’]) + ‘scissors’ ([‘rock’, ‘rock’, ‘paper’, ‘scissors’]).1
playedSoFar
-
DO USE CONCAT: Instead of using destructive
, we can use1
push
which creates a new array without mutating the original. The new array is passed as an argument into the recursive subroutine and the original is used to concat the next elements. This gives us what we want -1
concat
([‘rock’]) + ‘rock’ ([‘rock’, ‘rock’]),1
playedSoFar
([‘rock’]) + ‘paper’ ([‘rock’, ‘paper’]), and1
playedSoFar
([‘rock’]) + ‘scissors’ ([‘rock’, ‘scissors’])1
playedSoFar
This gives us the following code:
Exit Strategy - When is the Recursion Finished?
In this example, there are 3 rounds being played (represented by the three nested loops in the iterative function). We’re done with the recursion when we go through all the rounds and there are no more rounds left to play. If we have a variable called
that stores this information, we’ll be done when 1
roundsLeft
is zero. So 1
roundsLeft
turns into the following:1
combos_0
So
will need to be an argument passed into the recursive subroutine 1
roundsLeft
.1
getOutcomes
Keeping Track of Rounds Left
We’ll need to include
as an argument of the recursive function so we’ll modify the recurive call as such:1
roundsLeft
The Recursive Subroutine
Step Four: Replacing the Iterative For Loops with the Recursive Subroutine
After completing the steps above, we can put the recursive subroutine in where the nested for loops were in the iterative approach. And there is one more thing we have to do - call the recursive function with the appropriate arguments. In this case,
starts as the empty array and 1
playedSoFar
starts as 3, the number of rounds of play specified in this example.1
roundsLeft
Note: It is now trivial to make this function work with any number of rounds, not just 3. We can do this by allowing
to take an argument called 1
rockPaperScissors
that specifies the number of rounds of play. See the code below for this alternative:1
rounds