Another Function Decorator (for Computing Takl Recursive Functions) In this optional lecture note, we show a function and various decorators for executing tail recursive functions written in a special form. All the code is contained in an accompanying Python project file containing three modules ------------------------------------------------------------------------------ Direct Tail Recursion: First, a Functional In the functional programming lecture we discussed tail recursion and showed how the direct recursive list_sum function def list_sum (l : [int]) -> int: if l == []: return 0 else: return l[0] + list_sum(l[1:]) # 1st value + sum of all following values could be rewritten with an accumulator to become tail recursive def list_sum ( l : [int] ) -> int: def sum_tail(alist : [int], acc : int) -> int: if alist == []: return acc else: return sum_tail( alist[1:], alist[0]+acc ) return sum_tail(l,0) and then how it could be simplified to be iterative, ultimately appearing as def list_sum (l : [int]) -> int: al,acc = l,0 # mirrors the initial call of sum_tail while al != []: al,acc = al[1:], al[0] + acc # mirrors the recursive call to sum_tail return acc Many functional programming languages automatically recognize and perform such transformations, converting tail recursive functions into iterative ones (which use less space and often executer faster). Python doesn't recognize nor perform such transformations automaticlly; but we can use a function decorator, operating on a special form of tail recursive functions, to iteratively execute their code. To do so, we first translate the recursive function into a tail recursive one. We will simplify what we wrote above, using a default value parameter to account for the accumulator, and thus avoid the need for a nested function. def list_sum (l : [int], acc : int = 0) -> int: if l == []: return acc else: return list_sum( l[1:], acc+l[0] ) Now we translate each return into a special form: it does not return the correct value, but instead returns something a bit more a general: a tuple, whose first value is a boolean telling whether it represents a base case; if it does, the second value is the value that the function returns; if it is not a base case, the remaining values in the tuple are the arguments that would be passed to the recursive function call. Note, we don't call the function recursively. Here is how we would rewrite list_sum into this special form. def list_sum (l : [int], acc : int = 0) -> int: if l == []: return (True, acc) # Base case and value of function else: return (False, l[1:], acc+l[0]) # Recursive case and updated arguments In this special form, calling this function doesn't compute the correct answer. Instead it returns information that can be used to compute the correct answer. So, if the first value in the returned tuple is ... (1) True : the correct answer is the second value (2) False: the correct answer is a recursive call with updated arguments Given tail recursive functions written in this special form, we can write a functional that evaluates tall recursive functions as follows. def tail_recursive_executor(f,*args): while True: #print('calling',f.__name__,'with arguments:',*args) base_case, *args = f(*args) # parallel binding of two names if base_case: return args[0] With list_sum and tal_recursive_execotor, we can execute the following call (note: we pass the function, list_sum, and it argument(s) [5, 3, 1, 4] separately). print(tail_recursive_executor(list_sum,[5, 3, 1, 4])) With the print statement uncommented, this call produces the following trace and returned value. Note that no value for acc is initially passed to the call to list_sum, so no value is printed after the list in the first output (although it is defaulted to 0) calling list_sum with arguments: [5, 3, 1, 4] calling list_sum with arguments: [3, 1, 4] 5 calling list_sum with arguments: [1, 4] 8 calling list_sum with arguments: [4] 9 calling list_sum with arguments: [] 13 13 ------------------------------------------------------------------------------ Direct Tail Recursion: A Decorator We can improve on the functional: we can write a function decorator (here shown as a function; but it could also be written as a class) and use it to accomplish the same result more transparently. # Write the function decorator as a functional (the body of do_as_iterations is # similar to the functional above) def tail_recursive_executor(f): def do_as_iteration(*args): while True: print('performing',f.__name__,'on',*args) base_case, *args = f(*args) if base_case: return args[0] return do_as_iteration # Use it to decorate an appropriately written tail-recursive function @tail_recursive_executor def list_sum (l : [int], acc : int = 0) -> int: if l == []: return (True, acc) else: return (False, l[1:], acc+l[0]) # Call the decorated function print(list_sum([5, 3, 1, 4])) Here we rebind list_sum to the do_as_iteration function created and returned by the tail_recursive_executor function (applied with @ as a decorator), which means that we can directly call list_sum (which is now bound to do_as_iteration) with a list argument. ------------------------------------------------------------------------------ Indirect tail recursion: Generalizing the Special forma and Decorator Unfortunately, the code above works only with directly recursive functions. Examine the following mutually tail recursive functions. Hand simulate a call on is_odd(5) to see how it computes its result. Basically n is even/odd exactly when n-1 is odd/even, both with the base case of 0 being/non-being even/odd def is_even(n : int) -> bool: if n == 0: return True else: return is_odd(n-1) def is_odd(n : int) -> bool: if n == 0: return False else: return is_even(n-1) Note that neither of these function require accumulators to be tail recursive. With indirectly recursive functions, we have to generalize the special form a bit (and likewise the tail_recursive_executor). In the new special form, the tuple returned should include (in the second position) what function to call (when it isn't the base case). So, for list_sum we would rewrite it as def list_sum (l : [int], acc : int = 0) -> int: if l == []: return (True, acc) else: return (False, list_sum, (l[1:], acc+l[0])) For is_even and is_odd we would rewrite them as def is_even(n : int) -> bool: if n == 0: return (True, True else: return (False, is_odd, n-1) def is_odd(n : int) -> bool: if n == 0: return (True, False) else: return (False, is_even, n-1) Now we must write the tail_recursive_executor to account for the extra function information in the special form. It is a more complex than the previous one, because the do_as_iteration function must store and use an attribute that refers to the function for which it was created to be equivalent. def tail_recursive_executor(f): def do_as_iteration(*args): f = do_as_iteration.f # Get special function for this call while True: print('calling',f.__name__,'with arguments: ',*args) base_case, val_or_f, *args = f(*args) if base_case: return val_or_f else: f = val_or_f.f # Get special function for recursive call do_as_iteration.f = f return do_as_iteration So, if we write @tail_recursive_executor def is_even(n : int) -> bool: if n == 0: return (True, True else: return (False, is_odd, n-1) @tail_recursive_executor def is_odd(n : int) -> bool: if n == 0: return (True, False) else: return (False, is_even, n-1) is_even and is_odd each are bound to a different do_as_iteration function. And, is_even.f is bound to the is_even special form and is_odd.f is bound to the is_odd special form. It is these two special forms that are actually executed when inside each do_as_iteration (in the one line calling f(*args). Calling is_odd(5) with the print statement uncommented produces the following trace and returned value. calling is_odd with arguments: 5 calling is_even with arguments: 4 calling is_odd with arguments: 3 calling is_even with arguemnts: 2 calling is_odd with arguments: 1 calling is_even with arguments: 0 True We can also write this decorator as a class (with __init__ and __call__ methods) as follows: class tail_recursive_executor: def __init__(self,f): self._f = f def __call__(self,*args): f = self while True: print('calling',f._f.__name__,'with arguments:',*args) done, val_or_f, *args = f._f(*args) if done: return val_or_f ------------------------------------------------------------------------------ Trampolines The functional/decorators here implements something called trampolining. Standard tail recursive code (executing recursively) grows the call frame downward until the base case, and then shrinks it while returning the answer. Think of that as one big jump down and bounce back. Instead, the functional/decorator code is a bunch of short bounces up and back as the tail_recursive_executor loops around (sometimes bouncing between different mutually recursive functions).