TCO in Python with exceptions
I was wondering if it would be possible to implement infinite recursion (TCO) in Python by using exceptions to discard the intermediary stack frames, and IT IS POSSIBLE!
Here is how, more explanation after the code
class Unwind(Exception):
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
def tco(f):
unwind = False
def _inner(*args, **kwargs):
nonlocal unwind
if unwind:
raise Unwind(*args, **kwargs)
unwind = True
while True:
try:
result = f(*args, **kwargs)
except Unwind as s:
args = s.args
kwargs = s.kwargs
continue
else:
unwind = False
return result
return _inner
@tco
def recurse(x):
if x <= 0:
return "ok"
recurse(x - 1)
print(recurse(1001)) # ok
print(recurse(1001)) # ok
@tco
def sum(x, acc):
if x <= 0:
return acc
sum(x - 1, acc + x)
print(sum(1001, 0)) # 501501
So, recurse
is a function that receive an int and calls
itself n times that int. Is well known that python can
only do 1000 nested calls before raising the
RecursionError: maximum recursion depth exceeded in comparison
error message. This can be parameterized by sys.setrecursionlimit
function, but anyway, if you call recurse(1000)
with
the default settings (assuming that @tco
is not applied to it)
you for sure will get the RecursionError
, the same is for
sum
function above, if you try to get the sum of a number
greater than 1000 (without @tco
) it will fail with the same error.
Languages that do not have tail call optimization (TCO) will
add a stack frame at each function call, each stack frame
spend memory and at some point you ran out of stack, and
if your language keep stack frames in heap you would
get out of memory, or maybe you get an error like RecursionError
above.
Tail call optimization is a technique where the previous stack frame
is discarded, or reused, so the stack consumption is constant. But for
this to be possible the function call need to be in tail position,
this means that the function call is the last thing the function will
do before return, for exmaple: return foo(a,b,c)
. In this case we
can discard the current stack frame BEFORE calling foo
.
But how can we do this? Well, is known that exceptions unwind the
stack, so if I can jump from a inner call to the outter while keeping
the inner stack frame information, we can use this as a way to implement
TCO. And this is precisely what @tco
does.
When we find a nested call we raise Unwind
exception while passing
to it the current function arguments. This jumps to the previous call,
discarding one stack frame, we override the function arguments an call
it again.
When we dectect no Unwind
exception it means that the function
reached its stop condition, we reset the unwind
variable
and return.
That’s it, cheers!