# NOTATION: In this file "NLI" stands for Nested List of Integers

def not_none(x):
    return x is not None

def rec_max1(L:'NLI') -> 'int or None':
    '''The largest integer in the nested list L, or None if there the
    list contains no integers'''
    L1 = []
    for x in L:
        if isinstance(x,list):
            L1 += [rec_max1(x)]
        elif isinstance(x,int):
            L1 += [x]
    # This might be new to you: it returns the list that results
    # from removing every element x of L1 such that not_none(x) == False.
    # In python, functions are objects, so they can be passed as arguments
    # as we're doing here, and even returned by other functions (as we
    # do in make_rec_fn_on_NLI below)
    L2 = list(filter(not_none, L1))
    if len(L2) == 0:
        return None
    else:
        return max(L2)


def rec_max2(L:'NLI') -> 'int or None':
    '''Same as rec_max1, but using list comprehensions and lambda'''
    L1 = [(rec_max2(x) if isinstance(x,list) else x) for
          x in L] 
    L2 = list(filter(lambda x: x is not None, L1))
    return max(L2) if len(L2) > 0 else None
   


# Note that the "type" 'function NLI -> (int or None)' is completely
# informal; to Python it is just a string.
def make_rec_fn_on_NLI(post_rec:'function NLI -> (int or None)',
                            base_case:'function int -> int'):
    '''Make a recursive function F that operates on nested lists of 
    integers, that works roughly like this on an input list L: 
    (1) Apply the function base_case to each int in L, and do a recursive call to F on each list in L, and put those results in a (non-nested) list L1. 
    (2) Let L2 be the result of filtering out all the occurences of None in L1.
    (3) Return the result of applying the function post_rec to L2''.
    '''
    def rec_fn(L:'NLI') -> 'int or None':
        L1 = []
        for x in L:
            if isinstance(x,list):
                L1.append(rec_fn(x))
            elif isinstance(x,int):
                L1.append(base_case(x))
        L2 = list( filter(not_none, L1) )
        return post_rec(L2)
        
    return rec_fn



def make_rec_fn_on_NLI2(post_rec:'function NLI -> (int or None)',
                        base_case:'function int -> int'):   
    '''Same as make_rec_fn_on_NLI but using list comprehensions
    and lambda'''
    def rec_fn(L:'NLI') -> 'int or None':
        L1 = [(rec_fn(x) if isinstance(x,list) else base_case(x))
              for x in L]
        L2 = list(filter(lambda x: x is not None, L1))
        return post_rec(L2)
    return rec_fn



def identity(x):
    return x
def max_or_none(L):
    return (max(L) if len(L) > 0 else None)

# The following definitions of rec_max3 and rec_max4 are equivalent
# to the earlier definitions of rec_max1 and rec_max2.

rec_max3 = make_rec_fn_on_NLI(max_or_none, identity)

rec_max4 = make_rec_fn_on_NLI(lambda L: max(L) if len(L) > 0 else None,
                              lambda x: x)

print(rec_max1([1,2,[3,[7],4,5],-1]))
print(rec_max2([1,2,[3,[7],4,5],-1]))
print(rec_max3([1,2,[3,[7],4,5],-1]))
print(rec_max4([1,2,[3,[7],4,5],-1]))


# Remember summing the elements of a nested list of integers? 
# We can use make_rec_fn_on_NLI for that too!
rec_sum = make_rec_fn_on_NLI(sum, lambda x: x)
print(rec_sum([1,2,[3,4,5]]))


# Define the depth of a nested list of integers as follows: 
#   The depth of a list that contains only integers is 1.
#   The depth of a list that contains lists of depths d₁,...,dₖ is
#     max(d₁,...,dₖ) + 1.

# The following two definitions of functions rec_depth1 and rec_depth2 
# are equivalent. They both compute the depth of a nested list.

def max_plus_1(L:list):
    return max(L) + 1
def always_0(x:int):
    return 0
rec_depth1 = make_rec_fn_on_NLI( max_plus_1, 
                                always_0 )

rec_depth2 = make_rec_fn_on_NLI( lambda L: max(L) + 1, 
                                lambda x: 0 )

print(rec_depth1([1,[2],[[3]]]))
print(rec_depth2([1,[2],[[3]]]))

