Perl / perl5

šŸŖ The Perl programming language
https://dev.perl.org/perl5/
Other
1.92k stars 549 forks source link

[EXPERIMENT] try/catch control structure #18760

Open rjbs opened 3 years ago

rjbs commented 3 years ago

try{}catch{} was first released in perl v5.34.0 as an experimental feature. This issue tracks its progress toward the end of its experimental phase.

rwp0 commented 3 years ago

try{}catch{} was first released in perl v5.34.0 as an experimental feature. This issue tracks its progress toward the end of its experimental phase.

addressing/identifying the features by name in contents eg. try as in use feature 'try' could be more informative for users/developers to recognize what feature it is in code

leonerd commented 3 years ago

The current feature is fairly minimal in its ability. In 5.35 I hope to get around to importing more abilities from Syntax::Keyword::Try.

choroba commented 3 years ago

Also note that the feature is missing in documentation of experimental, although it is supported by it.

Leont commented 3 years ago

Also note that the feature is missing in documentation of experimental, although it is supported by it.

Good point, I guess I should double-check the rest too.

boftx commented 3 years ago

I hope this is the right place to offer a comment on the feature.

I have concerns that the new feature is using a syntax/style that breaks from the existing pattern for "eval" and Try::Tiny (and probably other as well) that allows "return" to only leave the block. I think the new practice of having it exit the surrounding method will be confusing given how well entrenched the existing behavior is. The new behavior requires using what I think has long been considered a bad practice, that of simply "falling off the bottom" in order to return a value. Granted, that is a common practice in some case, especially in Moo(se) "has" clauses for clarity, but is discouraged overall.

Also, I am curious why it was decided to base this on a module that is only used by 37 packages in CPAN (according to the reverse dependencies) as opposed to Try::Tiny which has over 1300 listed.

On a touchier note, I have the impression that the change in behavior is being made to make Perl look/feel more like other languages instead of adapting a long accepted solution to the underlying problem solved by Try::Tiny" and similar modules.

Thank you for your consideration.

Grinnz commented 3 years ago

For your consideration, Try::Tiny only was written not to do these things because it's not possible to do them in pure-perl; and it only has as many dependencies as it does because it was the only reasonable low-magic solution for a long time (and still is if you want to support Perl 5.10). These historical facts skew the usage in CPAN codebases, and of course should not be ignored but also should not hold hostage a feature to be less consistent with the rest of the language and user expectations (for example, if and foreach blocks which appear conceptually similar).

boftx commented 3 years ago

... should not hold hostage a feature to be less consistent with the rest of the language and user expectations (for example, if and foreach blocks which appear conceptually similar).

I view try/catch to be a more exact form of "eval" and so would think that the behavior of Try::Tiny is more correct. I also noticed in the original Syntax::Feature::Try that if the context variable is not supplied then the exception was available in $@, which makes me wonder if that module (and the new feature) still have the same problem that Try::Tiny resolved for the most part.

Grinnz commented 3 years ago

I view try/catch to be a more exact form of "eval" and so would think that the behavior of Try::Tiny is more correct.

As I said, Try::Tiny only had this behavior because there is no other option in pure-perl. It is not a form of "eval" syntax-wise because "eval" is an expression, not a statement (though it obviously shares some semantics).

SKT's usage of $@ does not cause the problems Try::Tiny is guarding against, because exceptions are indicated by running the "catch" block - Try::Tiny would frankly probably work fine using $@ itself, but that can't be changed now. $@ is more appropriate to use for a core feature in this case, though ideally most usage will assign it to a lexical variable within the "catch" block and not touch any global variables.

boftx commented 3 years ago

As I said, Try::Tiny only had this behavior because there is no other option in pure-perl.

FYI, I was informed by SSCAFFIDI that he wrote Try::Harder to specifically explore if Syntax::Feature::Try could be done in pure Perl and he says it was surprisingly easy to do so. So I would think that Try::Tiny, and others, kept the native behavior of "eval" because that was what was expected.

Grinnz commented 3 years ago

As I said, Try::Tiny only had this behavior because there is no other option in pure-perl.

FYI, I was informed by SSCAFFIDI that he wrote Try::Harder to specifically explore if Syntax::Feature::Try could be done in pure Perl and he says it was surprisingly easy to do so. So I would think that Try::Tiny, and others, kept the native behavior of "eval" because that was what was expected.

Try::Harder is a source filter, thus while easy to do, is not suitable for production code.

hercynium commented 3 years ago

Just to be clear - I didnā€™t say it was easy, just that it worked surprisingly well. In reality it was devilishly hard to get everything working just right: I actually copied the test suite from Syntax::Keyword::Try at the time and hacked on it until it passed everything but one test. IIRC, that was the ā€œreturn a valueā€ semantics, which I donā€™t care much for anyway. ā˜ŗ

oh, and I agree my source-filter approach is way, way less than ideal, but it really was just an experiment that turned out to work well enough I think it can still be useful to provide some sort of bridge for older versions of perl.

boftx commented 3 years ago

Just to be clear - I didnā€™t say it was easy, just that it worked surprisingly well. In reality it was devilishly hard ...

Thanks for the clarification! I should have just quoted you directly on that. :)

nrdvana commented 3 years ago

I happened to have this little brainstorm on a Perlmonks post, and it got some upvotes, so I'm sharing it here.

This is the try/catch (or rather, catch) that I wish we had in Perl:

sub foo {
  some_code();
  catch {
     print "ignoring $@";
     say "$_ would be the exact string thrown, without 'at lineā€¦'";
  }
}

sub bar {
  some_code();
  catch ClassName::Xyz { ... }
  catch /pattern/ { ... }
  catch (ref && ref->can("info")) { ... }

  catch my $e { say $e }
  catch my $e ($e isa 'Pkg') { ... }
  catch my $e (ref($e) && ref($e)->can("info")) { ... }

  my $val= do { might_fail(); catch { 42 } };
  $val= do { might_fail(); catch { undef } } // 42;
  $val= do { might_fail(); catch } // 42;

  catch (die) { ... )  # exception within exception-test generates a warning and counts as 'false'
}

My idea of how that would play out in C-land is that as soon as the parser saw a catch, it would immediately wrap the current block's optree-in-progress with a try-context, ... and then continue as normal, attaching each consecutive catch block as a handler of that try context. I also think it should localize $_ for both the catch logical test and the catch body so that users can take advantage of all the implied "topic" operations on the exception. A lexical could be offered with the same syntax as for my $x. I think that the parentheses should be optional for the two most common cases of a isa-test and a regex. A catch could of course have no test at all and just run a block. And as a final special case, a catch could have no test and no block; and if it was the last statement it would have the effect of returning an empty list.

I'd also like it if the error string did not have the "at FILE line X" attached to it when caught, but I'm guessing that's impossible since a __DIE__ handler would be called first and it would expect to see that. I just thought I'd add it to the wishlist in case anyone thought of a way to make it happen.

nrdvana commented 3 years ago

Oh, and it neatly sidesteps the arguments about compatibility with expectations from Try::Tiny because

Do or do not. There is no try

^ that line needs to be in perldoc, right? That should seal the argument.

Grinnz commented 3 years ago

This would be more difficult to implement in the parser, differs from the expected control structure other languages have, and has no benefits that I can think of and more complicated semantics. The ability for typed dispatch of catch is planned for the future.

nrdvana commented 3 years ago

No benefits?

Edit: I guess I didn't make the case for the value of having a complex conditional. If you catch and re-throw an exception, it triggers the __DIE__ handler a second time and may lose stack trace information, or just bombard the log output with useless implementation details. If you can avoid catching it in the first place, the exception reporting will be more accurate and reliable.

Grinnz commented 3 years ago

That ability has nothing to do with your proposed syntax and is planned anyway. Thus the only benefit is saving a level of indentation, which does not outweigh the added implementation and semantic complexity.

nrdvana commented 3 years ago

It's unclear to me how the current syntax of Syntax::Keyword::Try would both allow for the declaration of the lexical variable name and also allow free-form boolean expressions. It appears that catch ($var isa Class) and catch ($var =~ /pattern/) are both special parsed syntaxes. In a general expression, would it just assume that the first undeclared variable reference was the one the user wanted to create?

My proposal above could be thought of as 6 separate patches, the first of which is probably easy and unobtrusive, and the rest which would be unlikely unless a lot of people just really liked it.

If I wrote a patch for the first and it turned out to be simple and didn't break any unit tests, would there be interest in including that?

haarg commented 3 years ago

Making try optional definitely not acceptable.

Grinnz commented 3 years ago

I apologize for my previous curt responses but this feature is already being trialed and is not in need of total redesigns right now without a clear benefit. And as I said, most of the other features you propose are not intrinsically related and are already planned in the scope of the current design, so tying them together is unnecessary.

nrdvana commented 3 years ago

I assume the purpose of the trial (and this ticket) is to get community feedback, right? So my raw feedback was that I really like being able to return from the middle of a try block, but was dismayed that the convenient $x= try { migh_fail() }; has expanded into the awkward $x= do { try { might_fail() } catch ($e) {} }. I read a bit on it and understand there are technical reasons why it needs to be a block and not a statement, and then was trying to think of constructive ideas on how to reduce boilerplate. An optional try seemed like a novel perl-ish reduction. I'm maybe surprised at the negative reaction, but not offended. I recognize it's really late into the decision process.

shadowcat-mst commented 3 years ago

@nrdvana @boftx we've been thrashing this out on IRC and cpan for-fscking-ever and basically - I totally appreciate where both of you are coming from in theory, but in practice it just ain't going to work. Using the do block trick is way more consistent, way more sensible, and also if either of you want to look into the compiler code you'd need to do things otherwise, I'd recommend laying it at least a full handle of bourbon beforehand.

Also, for the record, I've been writing code using Feature::Compat::Try for a bit now and while it took me a minute to adapt to needing the do block, I've actually really enjoyed the consistency code wise (and remember I was around on #moose when Try::Tiny was invented, I'm not unfamiliar with this stuff).

Fundamentally, "when mst says this is a nice idea in theory but he spent weeks trying to figure a way to do it but concluded that even for him that would be way too much crack in practice, that's really quite a lot of crack" applies here, and while that's kind of an argument from authority (argument from insanity?) I hope that's sufficient on top of the more directly technical arguments delivered already to make you both comfortable that this is the right way to go.

Much love (but only because my liquor cabinet is currently well stocked), mst :D

leonerd commented 2 years ago

Another year, another release. Perl v5.36 retains the experimental status of this feature.

Newly added is the ability to do try ... catch ... finally { ... } blocks.

Still missing is

leonerd commented 6 months ago

We've now considered that basic try/catch syntax can be made non-experimental. Things like typed or conditional catch can be added as a separate feature and/or experiment. Any changes to how exceptions are handled should probably also apply to the $@ variable via eval, so again should be its own feature/experiment.

Since each keyword is implemented individually, we can leave the warning on the optional finally blocks, because those have questions around double-exception control flow for the same reason that defer remains experimental.

waterkip commented 4 months ago

We use Try::Tiny a lot at work, and I decided to try and use the try/catch experimental feature for a personal project. I'm a little bit bummed by the fact that a return statement in the try/catch/finally returns from the function. I think Try::Tiny is now easier to use than the experimental feature because of this behavior.

For example, I want to perform some action in a particular state of the catch and want to short-circuit the code. With Try::Tiny, you just return; with this feature, you must be explicit with if/else statements in the catch block. We need to write more code to do the exact same thing.

Grinnz commented 4 months ago

That is intentional, so that it functions like every other syntax construct built into the language like if/else, rather than introducing invisible subroutine scopes. I find the best way to introduce "breakpoints" in an if/else or now a try/catch is to abstract the functionality into a subroutine which can be returned from at any point.

Grinnz commented 4 months ago

That said, nothing's stopping you from continuing to use Try::Tiny if you prefer its quirks, but I wouldn't expect this behavior to change.

waterkip commented 4 months ago

I know I can continue to use Try::Tiny. This feature is an experiment, and I'm giving my feedback on it.