drym-org / qi

An embeddable flow-oriented language.
59 stars 12 forks source link

Designing exception handling (`try`) in Qi #29

Open countvajhula opened 2 years ago

countvajhula commented 2 years ago

Qi recently got a try form for exception handling. At the moment it is very basic -- a simple predicate-based dispatcher for exceptions encountered while attempting a flow on inputs.

(~> ("5")
    (try add1
      [exn:fail:contract? (~> ->number add1)]
      [exn? 0]))

Here are some proposed improvements to this form (comments welcome):

try..catch..else..finally

Python's try form supports an else clause that is only executed if no exception was encountered, and a finally clause that is always executed, whether an exception was encountered or not.

finally typically would not be needed since we would commonly use (~> (try ...) finally-flo), but in cases where an exception is likely to be re-raised within the try form, a finally flow could be useful. It is questionable, though, since unlike python where side effects and mutation are common, Qi flows typically accept input and produce output. A finally clause that is expected to be hit only when try is in the process of re-raising an exception, seems to suggest that this flow would only be used for side effects and mutation.

It probably wouldn't be hard to support finally if we wanted to, and in that case it would likely be treated as another "handler" flow just like any of the other handler clauses in the try statement. On the other hand, since its use is likely to be a fringe usecase in Qi (unlike python, and even there finally is already uncommon), its inclusion would perhaps encourage unidiomatic code (i.e. mutation and side effects) more often than it would be useful. Still, it may be better to give users the flexibility to do weird and unidiomatic things than to not trust them. "Idiom", it could be argued, is best encouraged by convention rather than by constraint. On the nth hand, well-designed constraints help the user find better ways to do things.

else seems useful for the same reasons as the python version.

Note that else here would mean, "if no exception was encountered" rather than "for any other exception that is encountered". For a catch-all exception, we would use exn? or even _.

Accessing the exception object in handler flows

Currently, each of the predicates in the try form receive the raised exception, while the corresponding handler flows receive the inputs to the try form. It may be that in some cases we would want to be able to manipulate the raised exception even in the handler flows, for instance, if we use data contained in the exception to compute an appropriate output, or if we re-raise a fresh exception based on the contained data.

There are a few different options here:

  1. First, we could support a (=> ...) form in handler flows, similarly to switch where using this form propagates the output of the predicate flow to the consequent flow as the first input.

  2. A second possibility is to support it via a syntax parameter of some kind, so that within the handler flow, a name like e could be used and it would be bound to the exception object. I'm not sure whether this would be straightforward to implement or not, since typically, syntax parameters need to be exported at the module level, and we only want this identifier to have meaning not just within Qi but in fact within a subform of Qi (specifically try). Using a syntax parameter I think would mean that whatever reserved word we use here (e.g. err) would need to be exported in the Racket namespace. That seems non-ideal. It may be that we could use binding spaces for this, so that we only need to export the binding in the qi binding space. Of course, we only need it in a subform of Qi rather than anywhere in Qi, so it's still a little more heavy-handed than would seem to be necessary, but it may be fine since we can pick an identifier that's unlikely to collide with anything else in Qi, at least.

  3. Another option is to support a subform (as ...) like (try flo [(as exn:fail err) handler-flo]) which would introduce the err binding in the handler flow. I'm not sure whether inserting the binding here would be straightforward or not, and it's possible this would require broader support for bindings in Qi as a whole (which is already planned, so this wouldn't discount this option).

We could also support a combination of the above.

Any other ideas?