Skip to content

Add recur.stackless, a generalisation of recur.tco. #81

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions fn/recur.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from collections import namedtuple

"""Provides decorator to deal with tail calls in recursive function."""

class tco(object):
Expand Down Expand Up @@ -41,3 +43,87 @@ def __call__(self, *args, **kwargs):
# impossible to use such function in tail calls loop?
if callable(act): action = act
kwargs = result[2] if len(result) > 2 else {}

class stackless(object):
"""Provides a "stackless" (constant Python stack) recursion decorator
for generators.

Invoking as f() creates the control structures. Within a
function, only use `yield f.call()` and `yield f.tailcall()`.

Usage examples:

Tail call optimised recursion with tailcall():

@recur.stackless
def fact(n, acc=1):
if n == 0:
yield acc
return
yield fact.tailcall(n-1, n*acc)

Non-tail recursion with call() uses heap space so won't overflow:

@recur.stackless
def fib(n):
if n == 0:
yield 1
return
if n == 1:
yield 1
return
yield (yield fib.call(n-1)) + (yield fib.call(n-2))

Mutual recursion also works:

@recur.stackless
def is_odd(n):
if n == 0:
yield False
return
yield is_even.tailcall(n-1)

@recur.stackless
def is_even(n):
if n == 0:
yield True
return
yield is_odd.tailcall(n-1)
"""

__slots__ = "func",

Thunk = namedtuple('Thunk', ("func", "args", "kwargs", "is_tailcall"))

def __init__(self, func):
self.func = func

def call(self, *args, **kwargs):
return self.Thunk(self.func, args, kwargs, False)

def tailcall(self, *args, **kwargs):
return self.Thunk(self.func, args, kwargs, True)

def __call__(self, *args, **kwargs):
s = [self.func(*args, **kwargs)]
r = []
while s:
try:
if r:
v = s[-1].send(r[-1])
r.pop()
else:
v = next(s[-1])
except StopIteration:
s.pop()
continue

if isinstance(v, self.Thunk):
g = v.func(*v.args, **v.kwargs)
if v.is_tailcall:
s[-1] = g
else:
s.append(g)
else:
r.append(v)
return r[0]