Hacker News new | ask | show | jobs
by simion314 2584 days ago
I personally did not understand the solution proposed here, I would like to see an actual solution to the problem that was raised.
1 comments

The idea is that rather than try/catch (which tries to do something and can report low-level failures but cannot resume them) one uses something more like Lisp's condition system. In a condition system, signalling an error preserves the stack, and it means for example that a low-level loop can detect an error (say, a poorly-formatted CSV cell), signal that to higher-level code, the high-level code can make a decision (e.g. to replace the contents of the cell or skip the cell or send an email to the CEO with the contents of the cell and wait for his reply) and the low-level loop can continue processing as if there had never been a problem at all. There's a really great explanation in Practical Common Lisp:

http://gigamonkeys.com/book/beyond-exception-handling-condit...

In the context of the example, I can easily imagine how the system might have processed most rows without issue, called out to a scoring system for most exceptional rows, and raised a few rows to human attention when even the scoring system couldn't figure it out.

This sounds like something that could be introduced into a lot of modern functional-ish VM based languages, in the form of restartable exceptions, it just .. hasn't.
There are two big things lacking in current languages: dynamic scope and macros.

Dynamic scope gives callers the ability to provide values to their callee's callees, without polluting the argument list. It's a tremendously valuable option, which is ignored by just about every language these days. Fortunately you can fake it, e.g. with Go's context.Context.

Macros help clean up the verbose mess of catch/throw or try/except or panic/defer/recover blocks and make the logic explicit rather than implicit. Without macros, you have to spell out all the boilerplate all the time. You can get part of the way there with anonymous functions, but they are also imperfect and verbose.

I think there's a lot of argument right now between folks who recognise the absolute necessity for macros and folks who don't understand it yet; so far as I can tell there's very little understanding of or apreciation for dynamic scope. This strikes me as unfortunate: while I definitely don't want it by default, it's wonderful in a lot of situations.

Is this lie instead of throwin errors/exceptions you raise Error events ?
Here's a stupidly-simple example in Python. So very not ready for production use:

    def with_handler(ctx, h):
        return {'handler': h, 'next': ctx}

    def with_restart(ctx, name, r):
        return {'restart': (name, r), 'next': ctx}

    def find_handler(ctx, err):
        if 'handler' in ctx:
            return ctx['handler']
        elif 'next' in ctx:
            return find_handler(ctx['next'], err)
        else:
            raise Exception('Unhandled error %s' % err)

    def find_restart(ctx, name):
        if 'restart' in ctx and ctx['restart'][0] == name:
            return ctx['restart'][1]
        elif 'next' in ctx:
            return find_restart(ctx['next'], name)
        else:
            return error(ctx, 'No restart named %s' % name)

    def error(ctx, err):
        find_handler(ctx, err)(ctx, err)
        raise Exception('No handler found for %s in %s', err, ctx)

    def do_stuff(ctx):
        results = []
        for i in range(100):
            results.append(do_little_stuff(ctx, i))
        return results

    class alpha(Exception):
        def __init__(self, text):
            self.text = text

    def do_little_stuff(ctx, i):
        def restart(ctx):
            raise alpha('text: %d' % i)
        ctx = with_restart(ctx, 'use-alpha', restart)
        try:
            if i % 3 == 0:
                error(ctx, '%d %% 3 == 0' % i)
            return i
        except alpha as e:
            return e.text

    def handler(ctx, err):
        find_restart(ctx, 'use-alpha')(ctx)
    ctx = with_handler({}, handler)
    print do_stuff(ctx)
Note how the top level provides a contextual handler which chooses to use the alpha restart, while the restart itself is provided by the mid-level do_stuff, while the low-level do_little_stuff both provides the restart & signals the condition.

Add in some type detection &c. and this could start to be useful. But it'd still be hellaciously verbose compared to the Lisp:

    (defun do-stuff ()
      (loop for i below 100 collect (do-little-stuff i)))
    
    (define-condition mod-three () ())
    
    (defun do-little-stuff (i)
      (restart-case
          (progn
            (when (zerop (mod i 3)) (error 'mod-three))
            i)
        (use-alpha () (format nil "text: ~d" i))))
    
    (handler-bind ((mod-three #'(lambda (c) (invoke-restart 'use-alpha))))
      (do-stuff))
Ah, like the old Visual Basic "ON ERROR" system? (Of course, that was nearly always used for ON ERROR RESUME NEXT)
To have this used by libraries and libraries users won't you need some standardization though in the language? Otherwise you get various implementation.
Yup, that's another reason I love Lisp — it's been standardised since 1994, and everyone supports it (and since it's standardised and provided by the implementation, there are efficiency hacks possible which you wouldn't want in normal library code).

Lisp has been doing things newer languages still don't do since before a lot of modern programmers were even born. And there are relatively few warts for that age (upcasing and the pathname functions are the warts which leap to mind)!

Yeah, something like that.

One way to implement that is that error-event-raising is just a function which examines the dynamic context for error handlers, and calls them until one transfers control (e.g. by throwing an actual exception). That's pretty simple to implement in any language which has exceptions, panics or similar control-transfer structures.

It gets more complex when you have restarts. The nice thing about Lisp is that all the complexity can be hidden with macros; with other languages you have to be explicit every time.

I agree this could be a solution for some problems but it seems to me that there are problems where it may not work. So you probably use a mix of approaches. How would you document a library function that uses the mechanism you mention? If the user is not handles the error(forgets about a type or a new type is added in an update) will the user notice the error happened?
Same way that you'd document exceptions in a library.

> If the user is not handles the error(forgets about a type or a new type is added in an update) will the user notice the error happened?

In Lisp, ERROR invokes the debugger if the condition is not handled. So an unhandled error condition pauses execution. One cool thing is that the user can use the debugger to invoke restarts. It's extremely nice.

There's also CERROR, which establishes a restart to return from itself — so some errors can be continued on from — WARN, which offers automatically, mufflable warning output, and SIGNAL, which offers non-erroneous condition signalling (i.e., while ERROR never returns, SIGNAL can).

It's really, really full-featured but also easy to use.