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.
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 whentry 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.
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:
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.
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.
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).
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.Here are some proposed improvements to this form (comments welcome):
try..catch..else..finally
Python's
try
form supports anelse
clause that is only executed if no exception was encountered, and afinally
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 thetry
form, afinally
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. Afinally
clause that is expected to be hit only whentry
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 thetry
statement. On the other hand, since its use is likely to be a fringe usecase in Qi (unlike python, and even therefinally
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 useexn?
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 thetry
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:
First, we could support a
(=> ...)
form in handler flows, similarly toswitch
where using this form propagates the output of the predicate flow to the consequent flow as the first input.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 (specificallytry
). 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 theqi
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.Another option is to support a subform
(as ...)
like(try flo [(as exn:fail err) handler-flo])
which would introduce theerr
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?