Comparison with False - something I don't understand

Mark Wooding mdw at distorted.org.uk
Mon Dec 6 15:58:13 EST 2010


Paul Rubin <no.email at 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]



More information about the Python-list mailing list