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

"""
Explanation: Chapter 6: Fruitful functions

Contents
- Return values
- Incremental development
- Composition
- Boolean functions
- More recursion
- Leap of faith
- One more example
- Checking types
- Debugging
- Exercises

This notebook is based on "Think Python, 2Ed" by Allen B. Downey <br>
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
End of explanation
"""


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' )


"""
Explanation: 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:
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.
Use temporary variables to hold intermediate values so you can display and check them.
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
End of explanation
"""


def sum_numbers_iterative( n ):
    # INSERT YOUR CODE HERE
    return 0

def sum_numbers_recursive( n ):
    # INSERT YOUR CODE HERE
    return 0

"""
Explanation: 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
Generalize the function to work with the alternative data
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?
End of explanation
"""


def multiply_iterative( n ):
    # INSERT YOUR CODE HERE
    return 0

def multiply_recursive( n ):
    # INSERT YOUR CODE HERE
    return 0

"""
Explanation: Write a function that implements multiplication using only addition using both an iterative and a recursive approach.  Which was easier?  Why?
End of explanation
"""


def is_prime_iterative( n ):
    # INSERT YOUR CODE HERE
    return 0

def is_prime_recursive( n ):
    # INSERT YOUR CODE HERE
    return 0

"""
Explanation: 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?
End of explanation
"""


