Paul Rubin <no.em...@nospam.invalid> writes: > You know, I've heard the story from language designers several times > over, that they tried putting resumable exceptions into their languages > and it turned out to be a big mess, so they went to termination > exceptions that fixed the issue.
That seems very surprising to me. > Are there any languages out there with resumable exceptions? Common Lisp and Smalltalk spring to mind. It's fairly straightforward to write one in Scheme. (Actually, implementing the Common Lisp one in terms of fluids, closures and blocks isn't especially difficult.) > Escaping to a debugger doesn't really count as that. Indeed not. > I guess one way to do it would be call a coroutine to handle the > exception, and either continue or unwind after the continue returns, > but doing it in a single-threaded system just seems full of hazards. It seems pretty straightforward to me. Handlers are simply closures; the registered handlers are part of the prevailing dynamic context. When an exception occurs, you invoke the handlers, most-recently registered first. A handler that returns normally can be thought of as `declining' to handle the exception; a handler that explicitly transfers control elsewhere can be thought of as having handled it. To make this work, all you need is: * a fluid list (i.e., one which is part of the dynamic context) of handlers, which you can build in pure Python if you try hard enough (see below); * closures to represent handlers, which Python has already, and; * a nonlocal transfer mechanism, and a mechanism like try ... finally to allow functions to clean up if they're unwound. We can actually come up with a nonlocal transfer if we try, by abusing exceptions. [The code in this article is lightly tested, but probably contains stupid bugs. Be careful.] class block (object): """ Context manager for escapable blocks. Write with block() as escape: ... Invoking the `escape' function causes the context body to exit immediately. Invoking the `escape' function outside of the block's dynamic context raises a ValueError. """ def __init__(me): me._tag = None def _escape(me, value = None): if me._tag is None: raise ValueError, 'defunct block' me.result = value raise me._tag def __enter__(me, value = None): if me._tag: raise ValueError, 'block already active' me._tag = type('block tag', (BaseException,), {}) me.result = value return me._escape def __exit__(me, ty, val, tb): tag, me._tag = me._tag, None return ty is tag This is somewhat brittle, since some intervening context might capture the custom exception we're using, but I don't think we can do significantly better. Implementing fluids badly is easy. Effectively what we'd do to bind a fluid dynamically is try: old, fluid = fluid, new ... finally: fluid = old but this is visible in other threads. The following will do the job in a multithreaded environment. import threading as T import weakref as W class FluidBinding (object): """Context object for fluid bindings.""" def __init__(me, fluid, value): me.fluid = fluid me.value = value def __enter__(me): me.fluid._bind(me.value) def __exit__(me, ty, val, tb): me.fluid._unbind() class Fluid (object): """ Represents a fluid variable, i.e., one whose binding respects the dynamic context rather than the lexical context. Read and write the Fluid through the `value' property. The global value is shared by all threads. To dynamically bind the fluid, use the context manager `binding': with myfluid.binding(NEWVALUE): ... The binding is visible in functions called MAP within the context body, but not in other threads. """ _TLS = T.local() _UNBOUND = ['fluid unbound'] _OMIT = ['fluid omitted'] def __init__(me, value = _UNBOUND): """ Iinitialze a fluid, optionally setting the global value. """ me._value = value @property def value(me): """ Return the current value of the fluid. Raises AttributeError if the fluid is currently unbound. """ try: value, _ = me._TLS.map[me] except (AttributeError, KeyError): value = me._value if value == me._UNBOUND: raise AttributeError, 'unbound fluid' return value @value.setter def value(me, value): try: map = me._TLS.map _, stack = map[me] map[me] = value, stack except (AttributeError, KeyError): me._value = value @value.deleter def value(me): me.value = me._UNBOUND def binding(me, value = _OMIT, unbound = False): """ Bind the fluid dynamically. If UNBOUND is true then make the fluid be `unbound', i.e., not associated with a value. Otherwise, if VALUE is unset, then preserve the current value. Otherwise, set it to VALUE. The fluid can be modified and deleted. This will not affect the value outside of the dynamic extent of the context (e.g., in other threads, or when the context is unwound). """ if unbound: value = me._UNBOUND elif value == me._OMIT: value = me.value return _FluidBinding(me, value) def _bind(me, value): try: map = me._TLS.map except AttributeError: me._TLS.map = map = W.WeakKeyDictionary() try: old, stack = map[me] stack.append(old) map[me] = value, stack except KeyError: map[me] = value, [] def _unbind(me): map = me._TLS.map _, stack = map[me] if stack: map[me] = stack.pop(), stack else: del map[me] Now we can say with fluid.binding(new): ... and all is well. So, how do we piece all of this together to make a resumable exception system? We're going to need to keep a list of handlers. We're going to be adding and removing stuff a lot; and we want to make use of the fluid mechanism we've already built, which will restore old values automatically when we leave a dynamic context. So maintaining a linked list seems like a good idea. The nodes in the list will look somewhat like this. class Link (object): def __init__(me, item, next): me.item = item me.next = next Our handlers are going to be simple functions which take exception objects as arguments. A more advanced handler might filter exceptions based on their classes. That's not especially difficult to do badly, but it's fiddly to do well and it doesn't shed much light on the overall mechanism, so I'll omit that complication. We'll want a fluid for the handler list. HANDLERS = Fluid(None) Now we want to run a chunk of code with a handler attached. This seems like another good use for a context manager. class handler (object): def __init__(me, func): me._func = func def __enter__(me): me._bind = FluidBinding(HANDLERS, Link(me.func, HANDLERS.value) me._bind.__enter__() def __exit__(me, ty, val, tb): return me._bind.__exit__(ty, val, tb) (Context managers don't compose very nicely. It'd be prettier with the contextmanager decorator.) Let's say that we `signal' resumable exceptions rather than `raising' them. How do we do that? def signal(exc): with HANDLERS.binding(): while HANDLERS.value: h = HANDLERS.value HANDLERS.value = h.next h.item(exc) Yes, if all of the handlers decline, we just return. This is Bad for errors, but good for other kinds of situations, so `signal' is a convenient substrate to build on. def error(exc): signal(exc) raise RuntimeError, 'unhandled resumable exception' def warning(exc): signal(exc) ## Crank up python's usual warning stuff Note also that handlers are invoked in a dynamic environment which doesn't include them or any handlers added since. Obviously they can install their own handlers just fine. Cool. Now how about recovery? This is where nonlocal transfer comes in. If a handler wants to take responsibility for the exception, it has to make a nonlocal transfer. Where should it go? Let's maintain a table of restart points. Again, it'll be a linked list. RESTARTS = Fluid(None) class restart (block): def __init__(me, name): me.name = name super(restart, me).__init__(me) def invoke(me, value = None): me._escape(value) def __enter__(me): me._bind = FluidBinding(RESTARTS, Link(me, RESTARTS.value)) me._bind.__enter__() return super(restart, me).__enter__() def __exit__(me, ty, val, tb): ## Poor man's PROG1. try: return super(restart, me).__exit__(ty, val, tb) finally: me._bind.__exit__(ty, val, tb) def find_restart(name): r = RESTARTS.value while r: if r.item.name == name: return r.item r = r.next return None Using all of this is rather cumbersome, and Python doesn't allow syntactic abstraction so there isn't really much we can do to sweeten the pill. But I ought to provide an example of this machinery in action. def toy(x, y): r = restart('use-value') with r: if y == 0: error(ZeroDivisionError()) r.result = x/y return r.result def example(): def zd(exc): if not isinstance(exc, ZeroDivisionError): return r = find_restart('use-value') if not r: return r.invoke(42) print toy(5, 2) with handler(zd): print toy(1, 0) Does any of that help? -- [mdw] -- http://mail.python.org/mailman/listinfo/python-list