"""

John Rachlin
DS 2000: Intro to Programming with Data

Filename: first_functions.py
    
Description: A brief introduction to functions

    
"""

#%%   A function is a named block of code.
# We can pass inputs ("parameters") to the function
# The function gives us back (or returns) an output
# Functions make our code modular and reusable and are
# ESSENTIAL to writing quality code!

def hello():
    """ A simple hello-world function """
    print("Hello World!")
    
# As with def main().... indentation (whitespace)
# delineates the block of code that is part of the
# function
    
    
hello()
hello()
hello()


#%% Hello takes on a parameter

def hello(name):
    """ Say hello to <name> """
    name = name.upper()
    print("Hello", name)
    
hello("John")
hello("Mary")
hello("Captain Kirk")

your_name = input("What is your name? ")
hello(your_name)
print(your_name) # "your_name" isn't changed
print(name) # "name" doesn't exist outside of function!

#%% python doesn't care what kind of parameter you give it
# But your program might crash with an error
# This is known as dynamic typing.   We don't 
# declare our datatypes up front - we can change
# things as we go.   Some programmers scoff at this!
# Python programmers like the flexibility that 
# this offers.   Others point out that it slows
# things down and makes code prone to bugs and
# unexpected crashes.   Lesson:  There is no single
# best programming language.  Learn a few.


hello(7)

# Obligatory XKCD cartoon: https://xkcd.com/353/




#%% Returning a value

def square(x):
    """ Compute x^2 """
    return x**2 # return passes value back to caller

s = square(5)
print(s)


#%% factorial: n! = n * (n-1) * (n-2) * ... * 3 * 2 * 1
# n! the number of permutations (arrangements / sequences)
# on n distinct objects

def fact(n):
    f = 1
    for i in range(1, n+1):
        f *= i
    return f

for x in range(25):
    print(x, fact(x))
    
#%% Fasten your seat belt
# Functions can call themselves!
# n! = n * (n-1)!

def fact(n):
    if n <= 1:
        return 1
    else:
        return n * fact(n-1)
    
for x in range(25):
    print(x, fact(x))
  


#%% Plot the factorial function!

import matplotlib.pyplot as plt
flist = [fact(x) for x in range(100)]
tlist = [10**x for x in range(100)] # powers of 10
plt.figure(figsize=(5,5),dpi=200)
plt.xlabel("x")
plt.ylabel("x!")
plt.yscale('log')
plt.plot(flist, label='x!')
plt.plot(tlist, label='10^x')
plt.legend()
plt.grid()


#%% Positional vs. Named Parameters


# named (default) parameters are optional when calling the function.  
# The parameters take on their
# default values if not specified
# All positional parameters must be listed before any named parameters.

def mypow(x, exp=2):
    return x ** exp


mypow(3, .5)


def myfunc(a, b, c, msg = 'Hello', name='World'):
    print("Sum: ", a + b + c, msg, name)
    
myfunc(1,2,3, name='John')


