Skip to content

[New Concept]: Recursion #3051

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 55 commits into from
May 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
0a9fbcb
Create instructions.append.md
bobahop May 14, 2022
7dfffa7
Create hints.md
bobahop May 14, 2022
ca8048a
Update exercises/practice/twelve-days/.docs/hints.md
bobahop May 15, 2022
764679a
Update exercises/practice/twelve-days/.docs/hints.md
bobahop May 15, 2022
79c640e
Update exercises/practice/twelve-days/.docs/hints.md
bobahop May 15, 2022
ce142dc
Update exercises/practice/twelve-days/.docs/instructions.append.md
bobahop May 15, 2022
7270444
Update exercises/practice/twelve-days/.docs/instructions.append.md
bobahop May 15, 2022
fef59e7
Update exercises/practice/twelve-days/.docs/instructions.append.md
bobahop May 15, 2022
d7deda7
Update exercises/practice/twelve-days/.docs/hints.md
bobahop May 15, 2022
d996001
Merge branch 'exercism:main' into patch-1
bobahop May 15, 2022
509db72
Update config.json
bobahop May 15, 2022
f76bc26
Create config.json
bobahop May 15, 2022
5c98804
Create links.json
bobahop May 15, 2022
57142ce
Create about.md
bobahop May 15, 2022
6f721fc
Create introduction.md
bobahop May 15, 2022
57a0642
Update introduction.md
bobahop May 15, 2022
e8d71b6
Update about.md
bobahop May 15, 2022
cc9efa0
Update about.md
bobahop May 15, 2022
15dee33
Update about.md
bobahop May 15, 2022
62d16ad
Update concepts/recursion/about.md
bobahop May 16, 2022
f5e49d5
Update concepts/recursion/about.md
bobahop May 16, 2022
f5d9fdf
Update concepts/recursion/about.md
bobahop May 16, 2022
c2504ab
Update concepts/recursion/about.md
bobahop May 16, 2022
fa41a1b
Update concepts/recursion/about.md
bobahop May 16, 2022
b11386b
Update concepts/recursion/about.md
bobahop May 16, 2022
c6e32d1
Update about.md
bobahop May 16, 2022
eef783e
Update about.md
bobahop May 16, 2022
bcb9d95
Update introduction.md
bobahop May 16, 2022
3ec7144
Update introduction.md
bobahop May 16, 2022
570ced8
Update about.md
bobahop May 16, 2022
16bdecb
Merge branch 'exercism:main' into patch-1
bobahop May 20, 2022
7ff581c
Update about.md
bobahop May 20, 2022
bc7af90
Update about.md
bobahop May 20, 2022
db6f3ab
Merge branch 'exercism:main' into patch-1
bobahop May 21, 2022
857ff8a
Update about.md
bobahop May 22, 2022
461e98a
Update concepts/recursion/about.md
bobahop May 23, 2022
29f76e9
Update concepts/recursion/about.md
bobahop May 23, 2022
c67737e
Update concepts/recursion/about.md
bobahop May 23, 2022
6ccacc9
Update concepts/recursion/about.md
bobahop May 23, 2022
1a82eef
Update concepts/recursion/about.md
bobahop May 23, 2022
cfc5dde
Update concepts/recursion/about.md
bobahop May 23, 2022
3149506
Update concepts/recursion/about.md
bobahop May 23, 2022
07a59d6
Update concepts/recursion/about.md
bobahop May 23, 2022
4e23107
Update concepts/recursion/about.md
bobahop May 23, 2022
9c4805b
Update about.md
bobahop May 23, 2022
0f2d560
Update about.md
bobahop May 24, 2022
4eb0190
Update concepts/recursion/about.md
bobahop May 25, 2022
4c2b88a
Update concepts/recursion/about.md
bobahop May 25, 2022
6a4a262
Update concepts/recursion/about.md
bobahop May 25, 2022
097ee34
Update concepts/recursion/about.md
bobahop May 25, 2022
f03295b
Update concepts/recursion/about.md
bobahop May 25, 2022
d98ff67
Update concepts/recursion/about.md
bobahop May 25, 2022
10437b0
Update about.md
bobahop May 25, 2022
2519879
Update concepts/recursion/about.md
bobahop May 25, 2022
c2e1993
Merge branch 'exercism:main' into patch-1
bobahop May 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions concepts/recursion/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"blurb": "Recursion repeats code in a function by the function calling itself.",
"authors": [
"bobahop"
],
"contributors": []
}
194 changes: 194 additions & 0 deletions concepts/recursion/about.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# About

Recursion is a way to repeatedly execute code inside a function through the function calling itself.
Functions that call themselves are know as _recursive_ functions.
Recursion can be viewed as another way to loop/iterate.
And like looping, a Boolean expression or `True/False` test is used to know when to stop the recursive execution.
_Unlike_ looping, recursion without termination in Python cannot not run infinitely.
Values used in each function call are placed in their own frame on the Python interpreter stack.
If the total amount of function calls takes up more space than the stack has room for, it will result in an error.

## Looping vs Recursive Implementation

Looping and recursion may _feel_ similar in that they are both iterative.
However, they _look_ different, both at the code level and at the implementation level.
Looping can take place within the same frame on the call stack.
This is usually managed by updating one or more variable values to progressively maintain state for each iteration.
This is an efficient implementation, but it can be somewhat cluttered when looking at the code.

Recursion, rather than updating _variable state_, can pass _updated values_ directly as arguments to the next call (iteration) of the same function.
This declutters the body of the function and can clarify how each update happens.
However, it is also a less efficient implementation, as each call to the same function adds another frame to the stack.

## Recursion: Why and Why Not?

If there is risk of causing a stack error or overflow, why would anyone use a recursive strategy to solve a problem?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If there is risk of causing a stack error or overflow, why would anyone use a recursive strategy to solve a problem?
If there is an "efficiency penalty" and risk of causing a stack error or overflow, why would anyone use a recursive strategy to solve a problem?

_Readability, traceability, and intent._
There may be situations where a solution is more readable and/or easier to reason through when expressed through recursion than when expressed through looping.
There may also be program constraints with using/mutating data, managing complexity, delegating responsibility, or organizing workloads.
Problems that lend themselves to recursion include complex but repetitive problems that grow smaller over time, particularly [divide and conquer][divide and conquer] algorithms and [cumulative][cumulative] algorithms.
However, due to Python's limit for how many frames are allowed on the stack, not all problems will benefit from a fully recursive strategy.
Problems less naturally suited to recursion include ones that have a steady state, but need to repeat for a certain number of cycles, problems that need to execute asynchronously, and situations calling for a great number of iterations.

## Looping vs Recursive Strategy: Indira's Insecurity

Indira has her monthly social security auto-deposited in her bank account on the **_second Wednesday_** of every month.
Indira is concerned about balancing her check book.
She is afraid she will write checks before her money is deposited.
She asks her granddaughter Adya to give her a list of dates her money will appear in her account.

Adya, who is just learning how to program in Python, writes a program based on her first thoughts.
She wants to return a `list` of the deposit dates so they can be printed.
She wants to write a function that will work for _any year_.
In case the schedule changes (_or in case other relatives want Adya to calculate their deposit schedules_), she decides the function needs to take an additional parameter for the _weekday_.
Finally, Adya decides that the function needs a parameter for _which weekday_ of the month it is: the first, second, etc.
For all these requirements, she decides to use the `date` class imported from `datetime`.
Putting all of that together, Adya comes up with:

```
from datetime import date


def paydates_for_year(year, weekday, ordinal):
"""Returns a list of the matching weekday dates.

Keyword arguments:
year -- the year, e.g. 2022
weekday -- the weekday, e.g. 3 (for Wednesday)
ordinal -- which weekday of the month, e.g. 2 (for the second)
"""
output = []

for month in range(1, 13):
for day_num in range(1, 8):
if date(year, month, day_num).isoweekday() == weekday:
output.append(date(year, month, day_num + (ordinal - 1) * 7))
break
return output

# find the second Wednesday of the month for all the months in 2022
print(paydates_for_year(2022, 3, 2))
```

This first iteration works, but Adya wonders if she can refactor the code to use fewer lines with less nested looping.
She's also read that it is good to minimize mutating state, so she'd like to see if she can avoid mutating some of her variables such as `output`, `month`, and `day_num` .

She's read about recursion, and thinks about how she might change her program to use a recursive approach.
The variables that are created and mutated in her looping function could be passed in as arguments instead.
Rather than mutating the variables _inside_ her function, she could pass _updated values as arguments_ to the next function call.
With those intentions she arrives at this recursive approach:

```
from datetime import date



def paydates_for_year_rec(year, weekday, ordinal, month, day_num, output):
"""Returns a list of the matching weekday dates

Keyword arguments:
year -- the year, e.g. 2022
weekday -- the weekday, e.g. 3 (for Wednesday)
ordinal -- which weekday of the month, e.g. 2 (for the second)
month -- the month currently being processed
day_num -- the day of the month currently being processed
output -- the list to be returned
"""
if month == 13:
return output
if date(year, month, day_num).isoweekday() == weekday:
return paydates_for_year_rec(year, weekday, ordinal, month + 1, 1, output
+ [date(year, month, day_num + (ordinal - 1) * 7)])
return paydates_for_year_rec(year, weekday, ordinal, month, day_num + 1, output)

# find the second Wednesday of the month for all the months in 2022
print(paydates_for_year_rec(2022, 3, 2, 1, 1, []))

```

Adya is happy that there are no more nested loops, no mutated state, and 2 fewer lines of code!

She is a little concerned that the recursive approach uses more steps than the looping approach, and so is less "performant".
But re-writing the problem using recursion has definitely helped her deal with ugly nested looping (_a performance hazard_), extensive state mutation, and confusion around complex conditional logic.
It also feels more "readable" - she is sure that when she comes back to this code after a break, she will be able to read through and remember what it does more easily.

In the future, Adya may try to work through problems recursively first.
She may find it easier to initially walk through the problem in clear steps when nesting, mutation, and complexity are minimized.
After working out the basic logic, she can then focus on optimizing her initial recursive steps into a more performant looping approach.

Even later, when she learns about `tuples`, Adya could consider further "optimizing" approaches, such as using a `list comprehension` with `Calendar.itermonthdates`, or memoizing certain values.

## Recursive Variation: The Tail Call

A tail call is when the last statement of a function only calls itself and nothing more.
This example is not a tail call, as the function adds 1 to the result of calling itself

```python
def print_increment(step, max_value):
if step > max_value:
return 1
print(f'The step is {step}')
return 1 + print_increment(step + 1, max_value)


def main():
retval = print_increment(1, 2)
print(f'retval is {retval} after recursion')

if __name__ == "__main__":
main()

```

This will print

```
The step is 1
The step is 2
retval is 3 after recursion
```

To refactor it to a tail call, make `retval` a parameter of `print_increment`

```python
def print_increment(step, max_value, retval):
if step > max_value:
return retval
print(f'The step is {step}')
return print_increment(step + 1, max_value, retval + 1)


def main():
retval = print_increment(1, 2, 1)
print(f'retval is {retval} after recursion')

if __name__ == "__main__":
main()

```

You may find a tail call even easier to reason through than a recursive call that is not a tail call.
However, it is always important when using recursion to know that there will not be so many iterations that the stack will overflow.

## Recursion Limits in Python

Some languages are able to optimize tail calls so that each recursive call reuses the stack frame of the first call to the function (_similar to the way a loop reuses a frame_), instead of adding an additional frame to the stack.
Python is not one of those languages.
To guard against stack overflow, Python has a recursion limit that defaults to one thousand frames.
A [RecursionError](https://docs.python.org/3.8/library/exceptions.html#RecursionError) exception is raised when the interpreter detects that the recursion limit has been exceeded.
It is possible to use the [sys.setrecursionlimit](https://docs.python.org/3.8/library/sys.html#sys.setrecursionlimit) method to increase the recursion limit, but doing so runs the risk of having a runtime segmentation fault that will crash the program, and possibly the operating system.

## Resources

To learn more about using recursion in Python you can start with
- [python-programming: recursion][python-programming: recursion]
- [Real Python: python-recursion][Real Python: python-recursion]
- [Real Python: python-thinking-recursively][Real Python: python-thinking-recursively]

[python-programming: recursion]: https://www.programiz.com/python-programming/recursion
[Real Python: python-recursion]: https://realpython.com/python-recursion/
[Real Python: python-thinking-recursively]: https://realpython.com/python-thinking-recursively/
[RecursionError]: https://docs.python.org/3.8/library/exceptions.html#RecursionError
[setrecursionlimit]: https://docs.python.org/3.8/library/sys.html#sys.setrecursionlimit
[divide and conquer]: https://afteracademy.com/blog/divide-and-conquer-approach-in-programming
[cumulative]: https://www.geeksforgeeks.org/sum-of-natural-numbers-using-recursion/
35 changes: 35 additions & 0 deletions concepts/recursion/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Introduction

Recursion is a way to repeat code in a function by the function calling itself.
It can be viewed as another way to loop/iterate.
Like looping, a Boolean expression or `True/False` test is used to know when to stop the recursive execution.
_Unlike_ looping, recursion without termination in Python cannot not run infinitely.
Values used in each function call are placed in their own frame on the Python interpreter stack.
If the total amount of function calls takes up more space than the stack has room for, it will result in an error.

```python
def print_increment(step, max_value):
if step > max_value:
return
print(f'The step is {step}')
print_increment(step + 1, max_value)


def main():
print_increment(1, 2)
print("After recursion")

if __name__ == "__main__":
main()

```

This will print

```
The step is 1
The step is 2
After recursion
```

There may be some situations that are more readable and/or easier to reason through when expressed through recursion than when expressed through looping.
1 change: 1 addition & 0 deletions concepts/recursion/links.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
5 changes: 5 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2526,6 +2526,11 @@
"slug": "raising-and-handling-errors",
"name": "Raising And Handling Errors"
},
{
"uuid": "b77f434a-3127-4dab-b6f6-d04446fed496",
"slug": "recursion",
"name": "Recursion"
},
{
"uuid": "d645bd16-81c2-4839-9d54-fdcbe999c342",
"slug": "regular-expressions",
Expand Down