Skip to content

Commit 2e8e481

Browse files
committed
refactor ecs to separate module
1 parent d188c3c commit 2e8e481

File tree

4 files changed

+130
-96
lines changed

4 files changed

+130
-96
lines changed

tour.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
dyn, \
1010
let, letrec, dlet, dletrec, blet, bletrec, \
1111
immediate, begin, begin0, lazy_begin, lazy_begin0, \
12-
trampolined, jump, looped, looped_over, SELF, setescape, escape
12+
trampolined, jump, looped, looped_over, SELF, \
13+
setescape, escape
1314

1415
def dynscope_demo():
1516
assert dyn.a == 2

unpythonic/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from .arity import *
2323
from .assignonce import *
2424
from .dynscope import *
25+
from .ec import *
2526
from .let import * # no guarantees on evaluation order (before Python 3.6), nice syntax
2627

2728
# guaranteed evaluation order, clunky syntax

unpythonic/ec.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
"""Escape continuations."""
4+
5+
__all__ = ["escape", "setescape"]
6+
7+
from functools import wraps
8+
9+
class escape(Exception):
10+
"""Exception that essentially represents an escape continuation.
11+
12+
Use ``raise escape(value)`` to escape to the nearest (dynamically) surrounding
13+
``@setescape``. The @setescape'd function immediately terminates, returning
14+
``value``.
15+
16+
Trampolined functions may also use ``return escape(value)``; the trampoline
17+
will then raise the exception (this is to make it work also with lambdas).
18+
19+
The optional ``tag`` parameter can be used to limit which ``@setescape``
20+
points see this particular escape instance. Default is to be catchable
21+
by any ``@setescape``.
22+
"""
23+
def __init__(self, value, tag=None):
24+
self.value = value
25+
self.tag = tag
26+
27+
def setescape(tag=None):
28+
"""Decorator. Mark function as exitable by ``raise escape(value)``.
29+
30+
In Lisp terms, this essentially captures the escape continuation (ec)
31+
of the decorated function. The ec can then be invoked by raising escape(value).
32+
33+
To make this work with lambdas, in trampolined functions it is also legal
34+
to ``return escape(value)``. The trampoline specifically detects ``escape``
35+
instances, and performs the ``raise``.
36+
37+
Technically, this is a decorator factory; the optional tag parameter can be
38+
used to catch only those escapes with the same tag. ``tag`` can be a single
39+
value, or a tuple. Single value means catch that specific tag; tuple means
40+
catch any of those tags. Default is None, i.e. catch all.
41+
42+
Multi-return using escape continuation::
43+
44+
@setescape()
45+
def f():
46+
def g():
47+
raise escape("hello from g") # the arg becomes the return value of f()
48+
print("not reached")
49+
return False
50+
g()
51+
print("not reached either")
52+
return False
53+
assert f() == "hello from g"
54+
55+
Escape from FP loop::
56+
57+
@setescape() # no tag, catch any escape instance
58+
def f():
59+
@looped
60+
def s(loop, acc=0, i=0):
61+
if i > 5:
62+
return escape(acc) # the argument becomes the return value of f()
63+
return loop(acc + i, i + 1)
64+
print("never reached")
65+
return False
66+
assert f() == 15
67+
68+
For more control, tagged escape::
69+
70+
@setescape("foo")
71+
def foo():
72+
@immediate
73+
@setescape("bar")
74+
def bar():
75+
@looped
76+
def s(loop, acc=0, i=0):
77+
if i > 5:
78+
return escape(acc, tag="foo")
79+
return loop(acc + i, i + 1)
80+
print("never reached")
81+
return False
82+
print("never reached either")
83+
return False
84+
assert f() == 15
85+
"""
86+
if tag is None:
87+
tags = None
88+
elif isinstance(tag, (tuple, list)): # multiple tags
89+
tags = set(tag)
90+
else: # single tag
91+
tags = set((tag,))
92+
93+
def decorator(f):
94+
@wraps(f)
95+
def decorated(*args, **kwargs):
96+
try:
97+
return f(*args, **kwargs)
98+
except escape as e:
99+
if tags is None or e.tag is None or e.tag in tags:
100+
return e.value
101+
else: # meant for someone else, pass it on
102+
raise
103+
return decorated
104+
return decorator
105+
106+
def test():
107+
# "multi-return" using escape continuation
108+
@setescape()
109+
def f():
110+
def g():
111+
raise escape("hello from g") # the argument becomes the return value of f()
112+
print("not reached")
113+
g()
114+
print("not reached either")
115+
assert f() == "hello from g"
116+
117+
# tests with @looped in tco.py to prevent cyclic dependency
118+
119+
print("All tests PASSED")
120+
121+
if __name__ == '__main__':
122+
test()

unpythonic/tco.py

Lines changed: 5 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -186,98 +186,16 @@ def baz():
186186
over fn.py's is the more natural syntax for the client code.
187187
"""
188188

189-
__all__ = ["SELF", "jump", "trampolined", "looped", "looped_over", "escape", "setescape"]
189+
__all__ = ["SELF", "jump", "trampolined", "looped", "looped_over"]
190190

191191
from functools import wraps
192192

193193
from unpythonic.misc import immediate
194+
from unpythonic.ec import escape
194195

195196
# evil inspect dependency, used only to provide informative error messages.
196197
from unpythonic.arity import arity_includes, UnknownArity
197198

198-
# mainly for use with the looping constructs
199-
class escape(Exception):
200-
"""Exception that essentially represents an escape continuation.
201-
202-
Use ``raise escape(value)`` to escape to the nearest (dynamically) surrounding
203-
``@setescape``. The @setescape'd function immediately terminates, returning
204-
``value``.
205-
206-
Trampolined functions may also use ``return escape(value)``; the trampoline
207-
will then raise the exception (this is to make it work also with lambdas).
208-
209-
The optional ``tag`` parameter can be used to limit which ``@setescape``
210-
points see this particular escape instance. Default is to be catchable
211-
by any ``@setescape``.
212-
"""
213-
def __init__(self, value, tag=None):
214-
self.value = value
215-
self.tag = tag
216-
217-
# mainly for use with the looping constructs
218-
def setescape(tag=None):
219-
"""Decorator. Mark function as exitable by ``raise escape(value)``.
220-
221-
In Lisp terms, this essentially captures the escape continuation (ec)
222-
of the decorated function. The ec can then be invoked by raising escape(value).
223-
224-
To make this work with lambdas, in trampolined functions it is also legal
225-
to ``return escape(value)``. The trampoline specifically detects ``escape``
226-
instances, and performs the ``raise``.
227-
228-
Technically, this is a decorator factory; the optional tag parameter can be
229-
used to catch only those escapes with the same tag. ``tag`` can be a single
230-
value, or a tuple. Single value means catch that specific tag; tuple means
231-
catch any of those tags. Default is None, i.e. catch all.
232-
233-
Example::
234-
235-
@setescape() # no tag, catch any escape instance
236-
def f():
237-
@looped
238-
def s(loop, acc=0, i=0):
239-
if i > 5:
240-
return escape(acc) # the argument becomes the return value of f()
241-
return loop(acc + i, i + 1)
242-
print("never reached")
243-
assert f() == 15
244-
245-
Tagged escape::
246-
247-
@setescape("foo")
248-
def foo():
249-
@immediate
250-
@setescape("bar")
251-
def bar():
252-
@looped
253-
def s(loop, acc=0, i=0):
254-
if i > 5:
255-
return escape(acc, tag="foo")
256-
return loop(acc + i, i + 1)
257-
print("never reached")
258-
print("never reached either")
259-
assert f() == 15
260-
"""
261-
if tag is None:
262-
tags = None
263-
elif isinstance(tag, (tuple, list)): # multiple tags
264-
tags = set(tag)
265-
else: # single tag
266-
tags = set((tag,))
267-
268-
def decorator(f):
269-
@wraps(f)
270-
def decorated(*args, **kwargs):
271-
try:
272-
return f(*args, **kwargs)
273-
except escape as e:
274-
if tags is None or e.tag is None or e.tag in tags:
275-
return e.value
276-
else: # meant for someone else, pass it on
277-
raise
278-
return decorated
279-
return decorator
280-
281199
@immediate # immediate a class to make a singleton
282200
class SELF: # sentinel, could be any object but we want a nice __repr__.
283201
def __repr__(self):
@@ -668,25 +586,17 @@ def baz():
668586
e.evenp(10000))
669587
assert t is True
670588

671-
# "multi-return" using escape continuation
672-
@setescape()
673-
def f():
674-
def g():
675-
raise escape("hello from g") # the argument becomes the return value of f()
676-
print("not reached")
677-
g()
678-
print("not reached either")
679-
assert f() == "hello from g"
680-
681589
# escape surrounding function from inside FP loop
590+
from unpythonic.ec import setescape
682591
@setescape()
683592
def f():
684593
@looped
685594
def s(loop, acc=0, i=0):
686595
if i > 5:
687596
return escape(acc) # trampolined functions may also "return escape(...)"
688597
return loop(acc + i, i + 1)
689-
print("never reached")
598+
print("not reached")
599+
return False
690600
assert f() == 15
691601

692602
# setescape point tag can be single value or tuple (tuples OR'd, like isinstance())

0 commit comments

Comments
 (0)