
# Let’s say we want to create a higher-order function that takes as input 
# some function f and returns a new function that for any input returns 
# twice the value of f:

def doubler(f):
    def g(x):
        return 2 * f(x)
    return g


def f1(x):
    return x + 1

g1 = doubler(f1)

print( f1(3) )
print( g1(3) )
print( f1(-1) )
print( g1(-1) )

# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
# Problem:
#
#   What if f() has 2 arguments ???
# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

def f2(x, y):
    return x + y

g2 = doubler(f2)

print( f2(1,2) )

try:
    print( g2(1, 2) ) 	# TypeError: g2() takes exactly 1 argument (2 given)
except TypeError:
    print("problem")

# #################################################################################
# What we need is a way to specify a function that takes arbitrary arguments. 
# We can do this with argument unpacking and a little bit of magic:
# #################################################################################

# Consider this interesting "magic" function:

def magic(*args, **kwargs):
    print("unnamed args:", args)
    print("keyword args:", kwargs)

magic(1, 2, 3, key="word", key2="word2")
magic(3, key="word", key2="word2")

# So, when we define a function like this:
#
#    def magic(*args, **kwargs):
#
# args  will be a **tuple** containing all the unnamed arguments in the call
# kargs will be a **dict**  containing all the named   arguments in the call


# #################################################################
# Interestingly, it works THE OTHER AROUND TOO...
#
# If you have a TUPLE or a LIST and a DICTIONARY, you can UNPACK THEM
# and PASS the result AS PARAMETERS:
# 

# Example:

def other_way_magic(x, y, z):
    return x + y + z

x_y_list = [1, 2]
z_dict = { "z" : 3 }        # MUST contain "z" as key !!!

print(other_way_magic(*x_y_list, **z_dict))             # 6





# #######################################################################
# We can use *args and **kargs to write a GENERAL double function:
#

def doubler_correct(f):
    """
    works no matter what kind of inputs f expects
    """

    def g(*args, **kwargs):
        """
        whatever arguments g is supplied, pass them through to f
        """
        return 2 * f(*args, **kwargs)

    return g


print("\n\nGeneral doubler:")

# Works with f1(x): 

def f1(x):
    return x + 1

g1 = doubler_correct(f1)

print( f1(3) )
print( g1(3) )
print( f1(-1) )
print( g1(-1) )


# ALSO works with f2(x,y): 

def f2(x, y):
    return x + y

g2 = doubler_correct(f2)

print( f2(1,2) )

print( g2(1, 2) )

