<jupyter_start><jupyter_text>Chapter 6: Fruitful functions***__Contents__- [Return values](Return-values)- [Incremental development](Incremental-development)- [Composition](Composition)- [Boolean functions](Boolean-functions)- [More recursion](More-recursion)- [Leap of faith](Leap-of-faith)- [One more example](One-more-example)- [Checking types](Checking-types)- [Debugging](Debugging)- [Exercises](Exercises)***_This notebook is based on "Think Python, 2Ed" by <NAME>_ [https://greenteapress.com/wp/think-python-2e/](https://greenteapress.com/wp/think-python-2e/)***  Return values- Some of the functions we have used produce, or __return__, a value, but- Most of the functions we have written are _void__, meaning they don’t return any value- We are going to now focus on writing functions that return something- The book refers to these as __fruitful functions__- A ``return`` statement ends the function immediately and gives the value of the expression following it to the caller- The expression can be complicated, or a simple variable holding a value- A variable is preferred as it simplifies debugging- Just because a function returns a value doesn't mean that the caller has to use or store it- Also, you should only have one return statement in a function (despite what the book says)- Multiple return statements can complicate debugging code- Consider the code below<jupyter_code>def do_something_multiple( x ):
    if( x < 0 ):
        # Do something complicated
        return -1
    elif( x > 10 ):
        # Do something complicated
        return 1
    else:
        return 0

def do_something_single( x ):
    # Create a value to hold the result
    result = 0
    
    if( x < 0 ):
        # Do something complicated
        result = -1
    elif( x > 10 ):
        # Do something complicated
        result = 1
    else:
        result = 0

    # Return the result
    return result<jupyter_output><empty_output><jupyter_text>- While both functions do the same thing, the second is easier to debug- The first function has three (3) potential paths to the function, meaning there are three paths that need to be checked in case of an error- The second function has only one way in and out of the function, meaning it is easier to track down an error  Incremental development- As you write more and longer functions, you will find yourself debugging more often- As the complexity increases, so does the probability of errors and the need for debugging- __Incremental development__ is a way of dealing with this complexity and reducing the chance of errrors- The goal is to only add small portions of code at a time and test them before moving on- The key aspects of the process are:  1. Start with a working program and make small, incremental changes.  At any point, if there is an error, you should have a good idea as to where it is.  1. Use temporary variables to hold intermediate values so you can display and check them.  1. Once the program is working, remove some of the scaffolding or consolidate multiple statements into compound expressions, but only if it doesn't make the program hard to read.- Displaying the algorithm's current progress is a simple form of __logging__- Adding print statements for crucial values as you write code is a form of __scaffolding__- This type of code helps you write the software, but isn't useful in the final version- __TODO__ Work hypotenuse example from the textbook (Ex. 6.2)  Composition- Building larger components from smaller components is called __composition__- Here, we see it as calling functions from within another function- We did this with the area of a donut function discussed in a previous chapter- Although the book says that we can remove the temporary variable ``radius`` and ``result`` in its ``circle_area`` function once we are done with development, I disagree- It makes the code much more readable and easier to debug/refactor in the future- The idea of composition is fundamental in programming and we will use it consistently  Boolean functions- Functions can also return ``boolean`` values- Again, _don't_ follow the boks example by having more than one ``return`` statement in a function- Boolean return values are common for functions that ask ``True``/``False`` or yes/no questions- Try to think of some questions of this type you may want to ask- These types of functions are often used in conditional statemetns- When you do this, don't test the result against ``True`` or ``False``- It is unnecessary and an opportunity to insert a logic error<jupyter_code>def is_between( x, y, z ):
    return (x <= y) and (y <= z)

print( is_between( 1, 2, 3) )
print( is_between( 1, 0, 3) )

# Do this
if( is_between( 1, 2, 3 ) ):
    print( 'It is between the values' )

# Don't do this
if( is_between( 1, 2, 3 ) == True ):
    print( 'It is between the values' )<jupyter_output>True
False
It is between the values
It is between the values<jupyter_text>More recursion- We have only covered a small portion of Python, but we have covered enough to implement any general algorithm- We are still missing things for input and output, but that’s it- To illustrate this point, the book develops a recursive function to compute factorials- Since we have covered additional recursive functions beyond countdown, we can move on  Leap of faith- Following the flow of execution is one way to read a program- However, that can quickly become tiresome- Another way is to skip over functions and assume they worked correctly- The author refers to this as a __leap of faith__- It could also be referred to as the __happy path__- You do this instinctively when you read code with a ``print`` or ``input`` functions- This can be especially helpful when you are writing a recursive function  Checking types- When we encountered the problem of floats and negative values as arguments to the ``countdown`` and ``fibonacci`` functions, we had two choices  1. Generalize the function to work with the alternative data  1. Check the parameters to ensure they are correct- Sometimes the first option will work, but- Most often the second is better- This is especially true for functions that have specific requirements regarding the validity of the arguments- Code that handles the check is referred to as the __guardian__- It protects the rest of the code from values that might cause an error- _What are some real-world examples?_  Debugging- Divide and conquer gives us many opportunities for debugging- We have seen this as we build smaller, helper functions and test them before building larger functions- If a function isn’t working, there are three possibilities to consider:  – There is something wrong with the arguments passed (a precondition is violated)  – There is something wrong with the function itself (a postcondition is violated)  – There is something wrong with the return value or the way it is used in client code- Using careful detective work, you can determine where the error lies- The first possibility can be tested by logging the function’s parameter values- The second possibility can be tested by logging out the state of the system as the function does its work- Also, call the function with arguments for which the correct output is known and testable- The third possibility can be tested by looking at how the function is being used and called  Exercises- Write a function that computes the sum of the numbers from 1 to ``n`` using both an iterative and a recursive approach.  Which was easier?  Why?<jupyter_code>def sum_numbers_iterative( n ):
    # INSERT YOUR CODE HERE
    return 0

def sum_numbers_recursive( n ):
    # INSERT YOUR CODE HERE
    return 0<jupyter_output><empty_output><jupyter_text>- Write a function that implements multiplication using only addition using both an iterative and a recursive approach.  Which was easier?  Why?<jupyter_code>def multiply_iterative( n ):
    # INSERT YOUR CODE HERE
    return 0

def multiply_recursive( n ):
    # INSERT YOUR CODE HERE
    return 0<jupyter_output><empty_output><jupyter_text>- Write a function that checks to see if a number ``n`` is a prime number using both an iterative and a recursive approach.  Which was easier?  Why?<jupyter_code>def is_prime_iterative( n ):
    # INSERT YOUR CODE HERE
    return 0

def is_prime_recursive( n ):
    # INSERT YOUR CODE HERE
    return 0<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.054812514927155484, 'fraction_numerical': 0.005254358729400526, 'mean_word_length': 4.169753086419753, 'pattern_counts': {'":': 0, '<': 25, '<?xml version=': 0, '>': 23, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '5', 'dir': 'jupyter-structured-clean-dedup', 'id': '415775', 'license': 'MIT', 'n_tokens_mistral': 2137, 'n_tokens_neox': 2012, 'n_words': 1239, 'path': 'CSNE2444-Intro-to-CS-I/jupyter-notebooks/ch06-fruitful-functions.ipynb', 'repo_name': 'snucsne/CSNE-Course-Source-Code', 'size': '8383'}