import math
e = math.exp(1.0)

e

"""
Explanation: Fruitful Functions
Return Values
Some of the built-in functions we have used, such as the math functions, produce results. Calling the function generates a value, which we usually assign to a variable or use as part of an expression.
End of explanation
"""


# Function that returns the area of cirle
def area(radius):
    temp = math.pi * radius**2
    return temp


"""
Explanation: All of the functions we have written so far are void; they print something but their return value is None. 
Now, we are (finally) going to write fruitful functions (Function with return value).
End of explanation
"""


def area(radius):
    return math.pi * radius**2

"""
Explanation: We have seen the return statement before, but in a fruitful function the return statement includes an expression. This statement means: “Return immediately from this function and use the following expression as a return value.”  The expression can be arbitrarily complicated, so we could have written this function more concisely:
End of explanation
"""


def absolute_value(x):
    if x < 0:
        return -x
    else:
        return x

"""
Explanation: On the other hand, temporary variables like temp often make debugging easier.
Sometimes it is useful to have multiple return statements, one in each branch of a conditional:
End of explanation
"""


def distance(x1, y1, x2, y2):
    dx = x2 - x1
    dy = y2 - y1
    print('dx is', dx)
    print('dy is', dy)
    return 0.0

distance(1,2,4,6)

"""
Explanation: Since these return statements are in an alternative conditional, only one will be executed.As soon as a return statement executes, the function terminates without executing any subsequent statements. Code that appears after a return statement, or any other place the flow of execution can never reach, is called dead code.
Try Yourself!
Write a compare function that returns 1 if x &gt; y, 0 if x == y, and -1 if x &lt; y.
Incremental Development
As you write larger functions, you might find yourself spending more time debugging. 
To deal with increasingly complex programs, you might want to try a process called incremental development. The goal of incremental development is to avoid long debugging sessions by adding and testing only a small amount of code at a time.
As an example, suppose you want to find the distance between two points, given by the
coordinates $(x_1, y_1)$ and $(x_2, y_2)$.
$$
distance = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}
$$
The first step is to consider what a distance function should look like in Python. In other words, what are the inputs (parameters) and what is the output (return value)? 
In this case, the inputs are two points, which you can represent using four numbers. The return value is the distance, which is a floating-point value. 
Already you can write an outline of the function:
Python 
def distance(x1, y1, x2, y2):
    return 0.0
Obviously, this version doesn’t compute distances; it always returns zero. But it is syntactically correct, and it runs, which means that you can test it before you make it more complicated.
We can start adding code to the body. A reasonable next step is to find the differences $x_2-x_1$ and $y_2-y_1$. The next version stores those values in temporary variables and prints them.
End of explanation
"""


def distance(x1, y1, x2, y2):
    dx = x2 - x1
    dy = y2 - y1
    dsquared = dx**2 + dy**2
    print('dsquared is: ', dsquared)
    return 0.0

distance(1,2,4,6)

"""
Explanation: If the function is working, it should display 'dx is 3' and 'dy is 4' for values (1,2,4,6) which will result (3,4,5). If so, we know that the function is getting the right arguments and performing the first computation correctly. If not, there are only a few lines to check.
Next we compute the sum of squares of dx and dy:
End of explanation
"""


def distance(x1, y1, x2, y2):
    dx = x2 - x1
    dy = y2 - y1
    dsquared = dx**2 + dy**2
    result = math.sqrt(dsquared)
    return result

distance(1,2,4,6)

"""
Explanation: Again, you would run the program at this stage and check the output (which should be 25). Finally, you can use math.sqrt to compute and return the result:
End of explanation
"""


def circle_area(xc, yc, xp, yp):
    return area(distance(xc, yc, xp, yp))

"""
Explanation: If that works correctly, you are done. Otherwise, you might want to print the value of result before the return statement.
The final version of the function doesn’t display anything when it runs; it only returns a value. The print statements we wrote are useful for debugging, but once you get the function working, you should remove them. Code like that is called scaffolding because it is helpful for building the program but is not part of the final product.
When you start out, you should add only a line or two of code at a time. As you gain more experience, you might find yourself writing and debugging bigger chunks. Either way, incremental development can save you a lot of debugging time.
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 where it is.


Use temporary variables to hold intermediate values so you can display and check them.


Once the program is working, you might want to remove some of the scaffolding or consolidate multiple statements into compound expressions, but only if it does not make the program difficult to read.


Try Yourself!
Use incremental development to write a function called hypotenuse that returns the length of the hypotenuse of a right triangle given the lengths of the two legs as arguments. Record each stage of the development process as you go.
Composition
As you should expect by now, you can call one function from within another. This ability is called composition.
As an example, we’ll write a function that takes two points, the center of the circle and a point on the perimeter, and computes the area of the circle. 
Assume that the center point is stored in the variables xc and yc, and the perimeter point is in xp and yp. The first step is to find the radius of the circle, which is the distance between the two points. We just wrote a function, distance, that does that:
Python
radius = distance(xc, yc, xp, yp)
The next step is to find the area of a circle with that radius; we just wrote that, too:
Python
result = area(radius)
Encapsulating these steps in a function, we get:
Python
def circle_area(xc, yc, xp, yp):
    radius = distance(xc, yc, xp, yp)
    result = area(radius)
    return result
The temporary variables radius and result are useful for development and debugging, but once the program is working, we can make it more concise by composing the function calls:
End of explanation
"""


def is_divisible(x, y):
    if x % y == 0:
        return True
    else:
        return False

"""
Explanation: Boolean Functions
Functions can return booleans, which is often convenient for hiding complicated tests inside functions. For example:
End of explanation
"""


is_divisible(6,4)

is_divisible(6,3)

"""
Explanation: It is common to give boolean functions names that sound like yes/no questions; is_divisible returns either True or False to indicate whether x is divisible by y.
End of explanation
"""


def is_divisible(x, y):
    return x % y == 0

"""
Explanation: The result of the == operator is a boolean, so we can write the function more concisely by returning it directly:
End of explanation
"""


x = 9
y = 14
if is_divisible(x, y):
    print('x is divisible by y')
else: 
    print('x is not divisible by y')

"""
Explanation: Boolean functions are often used in conditional statements:
End of explanation
"""


def factorial(n):
    if n == 0:
        return 1
    else:
        recurse = factorial(n-1)
        result = n * recurse
        return result
factorial(6)

"""
Explanation: Try Yourself!
Write a function is_between(x, y, z) that returns True if x &lt;= y &lt;= z or False
otherwise.
More Recursion
To give you an idea of what you can do with the tools you have learned so far, we’ll evaluate a few recursively defined mathematical functions. A recursive definition is similar to a circular definition, in the sense that the definition contains a reference to the thing being defined. A truly circular definition is not very useful:

vorpal: An adjective used to describe something that is vorpal

If you saw that definition in the dictionary, you might be annoyed. On the other hand, if you looked up the definition of the factorial function, denoted with the symbol !, you might get something like this:
$$
0! = 1
$$
$$
n! = n(n-1)!
$$
This definition says that the factorial of 0 is 1, and the factorial of any other value, n, is n
multiplied by the factorial of n-1.
So 3! is 3 times 2!, which is 2 times 1!, which is 1 times 0!. Putting it all together, 3! equals 3
times 2 times 1 times 1, which is 6.
If you can write a recursive definition of something, you can usually write a Python program to evaluate it. The first step is to decide what the parameters should be. In this case it should be clear that factorial takes an integer:
Python
def factorial(n):
If the argument happens to be 0, all we have to do is return 1:
Python
def factorial(n):
    if n == 0:
        return 1
Otherwise, and this is the interesting part, we have to make a recursive call to find the factorial of n-1 and then multiply it by n:
Python
def factorial(n):
    if n == 0:
        return 1
    else:
        recurse = factorial(n-1)
        result = n * recurse
        return result
End of explanation
"""


def fibonacci (n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fibonacci(n-1) + fibonacci(n-2)
    
fibonacci(12)

"""
Explanation: Let's define the fibonacci which is recursively defined mathematical function and one of the most common recursive introduction example:
End of explanation
"""


fibonacci(1.5)

"""
Explanation: If you try to follow the flow of execution here, even for fairly small values of n, your head explodes. But according to the leap of faith, if you assume that the two recursive calls work correctly, then it is clear that you get the right result by adding them together.
Checking Types
What happens if we call factorial and give it 1.5 as an argument?
End of explanation
"""


def factorial (n):
    if not isinstance(n, int):
        print('Factorial is only defined for integers.')
        return None
    elif n < 0:
        print('Factorial is not defined for negative integers.')
        return None
    elif n == 0:
        return 1
    else:
        return n * factorial(n-1)

"""
Explanation: It looks like an infinite recursion. But how can that be? There is a base case—when n == 0. But if n is not an integer, we can miss the base case and recurse forever.
In the first recursive call, the value of n is 0.5. In the next, it is -0.5. From there, it gets smaller (more negative), but it will never be 0.
We have two choices. We can try to generalize the factorial function to work with floating-point numbers, or we can make factorial check the type of its argument. The first option is called the gamma function and it’s a little beyond the scope of this book. So we’ll go for the second.
We can use the built-in function isinstance to verify the type of the argument. While we’re at it, we can also make sure the argument is positive:
End of explanation
"""


factorial("fred")

factorial(-2)

"""
Explanation: The first base case handles nonintegers; the second catches negative integers. In both cases, the program prints an error message and returns None to indicate that something went wrong:
End of explanation
"""


