-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
[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
[New Concept]: Recursion #3051
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 7dfffa7
Create hints.md
bobahop ca8048a
Update exercises/practice/twelve-days/.docs/hints.md
bobahop 764679a
Update exercises/practice/twelve-days/.docs/hints.md
bobahop 79c640e
Update exercises/practice/twelve-days/.docs/hints.md
bobahop ce142dc
Update exercises/practice/twelve-days/.docs/instructions.append.md
bobahop 7270444
Update exercises/practice/twelve-days/.docs/instructions.append.md
bobahop fef59e7
Update exercises/practice/twelve-days/.docs/instructions.append.md
bobahop d7deda7
Update exercises/practice/twelve-days/.docs/hints.md
bobahop d996001
Merge branch 'exercism:main' into patch-1
bobahop 509db72
Update config.json
bobahop f76bc26
Create config.json
bobahop 5c98804
Create links.json
bobahop 57142ce
Create about.md
bobahop 6f721fc
Create introduction.md
bobahop 57a0642
Update introduction.md
bobahop e8d71b6
Update about.md
bobahop cc9efa0
Update about.md
bobahop 15dee33
Update about.md
bobahop 62d16ad
Update concepts/recursion/about.md
bobahop f5e49d5
Update concepts/recursion/about.md
bobahop f5d9fdf
Update concepts/recursion/about.md
bobahop c2504ab
Update concepts/recursion/about.md
bobahop fa41a1b
Update concepts/recursion/about.md
bobahop b11386b
Update concepts/recursion/about.md
bobahop c6e32d1
Update about.md
bobahop eef783e
Update about.md
bobahop bcb9d95
Update introduction.md
bobahop 3ec7144
Update introduction.md
bobahop 570ced8
Update about.md
bobahop 16bdecb
Merge branch 'exercism:main' into patch-1
bobahop 7ff581c
Update about.md
bobahop bc7af90
Update about.md
bobahop db6f3ab
Merge branch 'exercism:main' into patch-1
bobahop 857ff8a
Update about.md
bobahop 461e98a
Update concepts/recursion/about.md
bobahop 29f76e9
Update concepts/recursion/about.md
bobahop c67737e
Update concepts/recursion/about.md
bobahop 6ccacc9
Update concepts/recursion/about.md
bobahop 1a82eef
Update concepts/recursion/about.md
bobahop cfc5dde
Update concepts/recursion/about.md
bobahop 3149506
Update concepts/recursion/about.md
bobahop 07a59d6
Update concepts/recursion/about.md
bobahop 4e23107
Update concepts/recursion/about.md
bobahop 9c4805b
Update about.md
bobahop 0f2d560
Update about.md
bobahop 4eb0190
Update concepts/recursion/about.md
bobahop 4c2b88a
Update concepts/recursion/about.md
bobahop 6a4a262
Update concepts/recursion/about.md
bobahop 097ee34
Update concepts/recursion/about.md
bobahop f03295b
Update concepts/recursion/about.md
bobahop d98ff67
Update concepts/recursion/about.md
bobahop 10437b0
Update about.md
bobahop 2519879
Update concepts/recursion/about.md
bobahop c2e1993
Merge branch 'exercism:main' into patch-1
bobahop File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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": [] | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||||||
|
||||||
BethanyG marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
## 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? | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
_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. | ||||||
bobahop marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
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. | ||||||
BethanyG marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
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. | ||||||
BethanyG marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
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! | ||||||
|
||||||
BethanyG marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
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/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
BethanyG marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
[] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.