On Dec 6, 12:58 pm, m...@distorted.org.uk (Mark Wooding) wrote: > 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?
You could do that. Or, you could just put your try...finally inside a loop. Carl Banks -- http://mail.python.org/mailman/listinfo/python-list