Skip Menu |

This queue is for tickets about the Syntax-Keyword-Try CPAN distribution.

Report information
The Basics
Id: 123918
Status: open
Priority: 0/
Queue: Syntax-Keyword-Try

People
Owner: Nobody in particular
Requestors: DBOOK [...] cpan.org
Cc:
AdminCc:

Bug Information
Severity: (no value)
Broken in: (no value)
Fixed in: (no value)



Subject: (Discussion) Exception dispatch
While this module clearly is designed to work like the try/catch/finally syntax in many other languages, exception dispatch in 'catch' is a more complex topic in Perl and thus not implemented yet. I am collecting here the current discussion and my thoughts so far. 1. The usual expected syntax based on object- and type-heavy languages is: catch (ExceptionClass $e) { ... } TryCatch, Syntax::Feature::Try, Error, and Try::Tiny::ByClass support this sort of dispatch. This could also be done here, but they don't use any common dispatch mechanism other than ->isa (mostly since they are quite different in underlying implementation). Additionally, in Perl, unlike most languages that make this feature popular, exceptions are commonly also bare strings. 2. Bare strings could be dispatched by providing a regex: catch (qr/.../ $e) But that looks kind of weird compared to the typed dispatch. 3. It could instead take less magical class-name strings or regexes: catch ('ExceptionClass') { ... } catch (qr/.../) { ... } This could use $@ directly like a normal catch block, or a lexical could be provided by a separate part of the syntax, like: catch ('ExceptionClass') as $e { ... } 4. Dispatch could also be done by smartmatching on provided type objects with smartmatch overload. This would be very similar to Function::Parameters types for example. use Types::Standard 'Int', 'InstanceOf'; catch (Int $e) { ... } catch (InstanceOf['ExceptionClass'] $e) { ... } However, exceptions are always strings or references, so a lot of the types are useless, and I don't see any value in making the common useful cases more complicated, just to be able to dispatch to hashref or arrayref or other weird exceptions. On the other hand, it would make this possible automatically: catch (InstanceOf['ExceptionClass', 'OtherClass']) { ... } 5. TryCatch provides an additional mechanism where you can provide an arbitrary boolean expression, but this also seems unnecessary for the majority of cases and adds some syntax confusion. catch (ExceptionClass $e where { $_->is_wobbly }) 6. Regardless of syntax, there is no way to allow an exception to propagate from the original exception context if conditional `catch` blocks are provided and it does not match any of them. This means it would have to require that a non-conditional `catch` block is also present, or otherwise differ from the usual semantics which goes against what this module is trying to do.
I like this idea very much. Every time i work with Python i wish we would steal the way they handle exceptions. try { ... } catch (SomeException, SomeOtherException) { ... } catch { ... } Especially considering that try {} blocks already don't prevent exceptions from being propagated to outer try {} blocks if they are not handled. try { try { ... } catch (SomeException) { ... } } catch (SomeOtherException) { ... } That just feels right.
On Thu Jan 18 05:33:36 2018, SRI wrote: Show quoted text
> I like this idea very much. Every time i work with Python i wish we > would steal the way they handle exceptions.
That mostly works in Python because all core python-raise exceptions are classy too. Here in perl, core perl exceptions are simple strings without class. Show quoted text
> Especially considering that try {} blocks already don't prevent > exceptions from being propagated to outer try {} blocks if they are > not handled.
Yes; I'm aware of the semantics there, but right now I can't provide those in perl. The problem is that an eval{} block (as implemented by the slightly unfortunately-named OP_ENTERTRY) does not distinguish catching types. It indiscriminately catches everything that's thrown its way. I can't therefore provide the "uncaught exceptions propagate upwards" behaviour at the moment. A workaround of sorts is to simply rethrow them, but this has nonideal properties in it - most notably that caller() inside a $SIG{__DIE__} can distinguish the difference. This leads to much confusion and misleading output in debugging and similar situations. I do though have a longterm plan to sometime this year start looking at implementing this syntax right there in actual perl core, aiming that one day on a suitably-new version of perl that use Syntax::Keyword::Try; becomes a oneline pureperl wrapper that just enables whatever feature bit is required to turn on the core syntax. Being implemented actually in core, would then allow a defaultless 'catch' to decline certain exceptions and send them higher up the stack unmolested. -- Paul Evans
Show quoted text
> That mostly works in Python because all core python-raise exceptions > are classy too. Here in perl, core perl exceptions are simple strings > without class.
Yes, and i doubt Perl core exceptions will ever get classes. Backwards compatibility would be a nightmare. But catch could still support them with a blessed/isa check, to encourage CPAN modules to use exception classes. try { ... } catch (SomeException) { ...handle exception of a specific class here... } catch { ...fallback for arbitrary exceptions, which may or may not be objects... }
Show quoted text
> Yes; I'm aware of the semantics there, but right now I can't provide > those in perl. The problem is that an eval{} block (as implemented by > the slightly unfortunately-named OP_ENTERTRY) does not distinguish > catching types. It indiscriminately catches everything that's thrown > its way. I can't therefore provide the "uncaught exceptions propagate > upwards" behaviour at the moment. A workaround of sorts is to simply > rethrow them, but this has nonideal properties in it - most notably > that caller() inside a $SIG{__DIE__} can distinguish the difference. > This leads to much confusion and misleading output in debugging and > similar situations.
That's a bummer. It would make exceptions so much more comfortable to work with.
I've been giving this some thought, during design of first my `isa` operator now in core perl, and second on a possible replacement for smartmatch, and I've now decided that actually exception dispatch is quite a different scenario from general value dispatch, that it might want different rules. In summary; exception dispatch tends only to need `isa` class, or string regexp testing, but often needs them both in the same place. Whereas a more general type/value dispatch system could require all sorts of different kinds of tests, but usually only one kind in any given place. I think therefore they're different enough to justify trying to solve them independently. As to an actual design: Initially, it might feel reasonable to simply put type names or regexps in parens, but that causes trouble for deferred values stored in lexicals. It'd be nice to know what kind of test to perform at compiletime, so we can reject bad ones, but this wouldn't work simply: my $x = "Some::X::Class"; # or qr/^A message string /; ... catch ($x) { ... } It would be far better then to make the programmer supply the testing operator (`isa` or `=~`) as part of the syntax; they can test against $@ catch ($@ isa Some::X::Class) { ... } catch ($@ =~ m/^A message string /) { ... } catch ($@ isa $otherclass) { ... } By noting a kind of symmetry with the way that sub signatures introduce a new variable just by naming it, maybe this also lets us have a new lexical: catch ($e isa Some::X::Class) { ... } catch ($e =~ m/^A message string /) { ... } And just to be clear - these would be specialcases of grammar. We wouldn't allow arbitrary expressions inside the parens; it must be one of catch (VAR isa EXPR) { STMTS... } catch (VAR =~ REGEXP) { STMTS... } Where VAR is either a new lexical, or the special $@. Possibly we'd also allow an un-guarded catch (VAR) { STMTS... } to catch any kind of exception and allocate a new variable. This feature would then be a replacement of the currently-experimental `catch my $e` of 0.11_022, which we'd remove. -- Paul Evans
On Tue Dec 10 19:29:30 2019, PEVANS wrote: Show quoted text
> catch (VAR isa EXPR) { STMTS... } > catch (VAR =~ REGEXP) { STMTS... } > > Where VAR is either a new lexical, or the special $@. Possibly we'd > also allow an un-guarded > > catch (VAR) { STMTS... } > > to catch any kind of exception and allocate a new variable.
I like these forms, I wonder if there should be a way to specify multiple conditions for one catch block. This complicates things since VAR should be the same for every condition. I think the most common case of this would be specifying multiple "isa" possibilities (since you can do some composition already with regexes at least). -Dan
I had initially been in favor of the simplest form that could handle classes and regexes, but I eventually realized that having some form of type or callback based system could be quite useful. Given that core perl is unlikely to use exception objects any time soon (or ever), it would be useful to write a type library that has regexes for the core errors thrown by perl, or other stringy errors. Consider something like the following: use Type::Utils -all; use Types::Standard qw(Str); declare "ModuleLoadFailure", as Str, where { /\ACan't locate / }; use Syntax::Keyword::Try; try { require Oh::No; } # no good ideas on syntax catch (ModuleLoadFailure $e) { ...; }
Further thoughts on this design, copied from my recent email to perl5-porters: Now we have the `isa` operator, it can easily be spelled out like an if() condition: try { ... } catch ($@ isa X::Unhandled) { uses class derivation test } catch ($@ =~ m/^Unhandled /) { uses string regexp pattern match } This syntax looks nice but doesn't gel nicely with my other syntax idea, which assigns the exception into a new lexical thus avoiding the fragile nature of global $@: try { ... } catch my $e { ... } Simply combined, the programmer has to specify $e twice in a redundant manner: catch my $e ($e isa Oopsie) { ... } A final thought is if we consider the contents of the parens not as a generic condition expression but instead as a very limited "only this syntax allowed" then we could perhaps contemplate allowing catch (my $e isa Oopsie) { ... } but that feels a bit awkward; introducing a new variable as part of a binary comparison operator yet expecting it to have value; internally its semantics operate like if((my $e = $@) isa Oopsie) { ... } To be clear here: the syntax is the only real sticking point here. If we can nail that down, then the semantics of the operation ought to be fairly obvious from there and I can get on and finish implementing the thing. -- Paul Evans
My latest thinking on this front may involve some syntax such as: try { ... } catch my $e (isa Some::Exception::Class) { ... }, (=~ m/^An error message /) { ... } Or maybe the `catch` keyword would be repeated per line, though that then involves repeating the error variable name also: try { ... } catch my $e (isa Some::Exception::Class) { ... } catch my $e (=~ m/^An error message /) { ... } At first glance those hanging operators at the start of the open paren feel very strange. The `isa` one isn't too bad but the string regexp bind `=~` look odd. But them more I work with it the more I am getting used to it. I don't know if it's just Stockholm Syndrome, or whether it actually isn't all that bad. To be clear here, I don't mean to implement arbitrary condition checking and subtly injecting the "$e" to form the lefthand side of any comparison operator. I mean to support only and exactly those two forms, at first; with semantics behaving *as if* the code was written like catch my $e { if ($e isa Some::Exception::Class) { ... } elsif($e =~ m/^An error message /) { ... } else { die $e } # to rethrow an unhandled exception } By restricting it to those two forms initially it leaves plenty of space open for possible additions later on, though I think already these two basic types will be enough to cover most use-cases. -- Paul Evans
Ugh. Not five minutes after I post that, ilmari makes a good point: 18:20 * ilmari would prefer it to look like a single-scalar signature Which lead me on to thinking catch ($e isa ClassName) { ... } catch($e =~ m/^Pattern /) { ... } catch ($e) { anything else } So maybe right now the syntax for `catch my VAR` should be catch ($e) { ... } We can also steal the `where`-type subkeyword from sub signatures, and allow things like: catch ($e where { ... }) { ... } catch ($e isa Future::Exception where {$e->category eq "resolve"}) { ... } This might be neat enough. In any case, it convinces me that `catch my VAR` is probably not a good longterm solution. Since I've written it anyway I'm going to release it now with big experimental warnings on, but it may come out again in the next version. -- Paul Evans
On Mon Jun 29 14:03:43 2020, PEVANS wrote: Show quoted text
> So maybe right now the syntax for `catch my VAR` should be > > catch ($e) { ... }
0.14 now makes this the suggested experimental syntax, and the previous short-lived `catch my VAR` is now deprecated. -- Paul Evans
On Wed Jul 08 07:08:54 2020, PEVANS wrote: Show quoted text
> 0.14 now makes this the suggested experimental syntax, and the > previous short-lived `catch my VAR` is now deprecated.
Additionally, version 0.15 now implements some conditional catch syntax. The unit tests are: https://metacpan.org/source/PEVANS/Syntax-Keyword-Try-0.15/t/04catch-types.t -- Paul Evans
On Mon Jul 20 19:18:31 2020, PEVANS wrote: Show quoted text
> > 0.14 now makes this the suggested experimental syntax,
> Additionally, version 0.15 now implements some conditional catch > syntax.
Very Nice ! Wishing i could defer recommending a Try-Catch module to a client until you settle the experimental features.
On Mon Jul 27 15:23:15 2020, BRICKER wrote: Show quoted text
> On Mon Jul 20 19:18:31 2020, PEVANS wrote:
> > > 0.14 now makes this the suggested experimental syntax,
> > Additionally, version 0.15 now implements some conditional catch > > syntax.
> > Very Nice ! Wishing i could defer recommending a Try-Catch module to a > client until you settle the experimental features.
Or alternatively: If you want to take part and help experiment with it, then the more this gets tried out and feedback is returned, the more confident I'll be about de-experimenting it. :) -- Paul Evans