Pre-RFC: flexible `try fn`

Again, factually wrong: Try in std::ops - Rust

Okay, there is a nightly trait. Thank you for pointing that out.

For the same reason why we have ? operator instead of try!, which too “saves bunch of characters and two braces”.

Now, this is completely different.

  1. It's not just bunch of letters, it's a large 5-lines match with lots of code.
  2. You are likely to error propagate on each line, that makes code with Try trait 5x times smaller and readable.

There is a point where saving bunch of characters stops to be valuable enough to implement it. Allow me to write ? instead of complex match. Okay, I buy it. Allow me to write throw err instead of return Err(err)? Thank you, not today.

Look at these examples:

if condition {
    return Err(Foo)
} else {
    return Ok(Bar)
}

and it's replaced by:

if condition {
    throw Foo
} else {
    return Ok(Bar)
}

Is it better in any way? I don't think so. I see function definition that returns Result<Foo,Bar>. I see honest return Ok(Foo) and return Err(Bar), they match function result type Result<Foo,Bar> which I know is Either[Ok(Foo), Err(Bar)] . As a result it's clear for me what happens here, it's consistent and nice. Why whould I like to have this throw? Because I want to save four chars and two braces?


More consideration: how this function is called? I believe it's

match foobar() {
   Ok(foo) => { ... }
   Err(bar) => { ... }
}

which again is clear because we either construct Ok(foo) or Err(bar) and deconstruct them in the same manner. We don't write

match foobar() {
   Ok(foo) => { ... }
   catch bar => { ... }
}

While it's reasonable to expect that you can catch something you have been thrown. But you can't. At least, at this point. But nothing to stops us from implementing it a bit later, when throw gets implemented and they create an RFC with "well, people is confused with throw Err that cannot be catched, so let's add this one too

It has completely not enough value to be a good language change.

I need to take a break. I just appeared in the thread because @td_ says that there is only bunch of peoples that disagree with proposal. It's not true, we are just silent.

12 Likes

I don’t mean to be rude, but I do not want rehash already held discussion, especially if you haven’t even bothered to read the proposal (because again in your post you have mistakes) and judged it by several comments in this thread.

1 Like

He was saying that the ? operator (which replaced the Try! macro) on saved "Try!( ... )" (i.e. 4 characters and 2 braces). Try!( ... ) existed first to replace the match and, in fact, the ? operator pretty much de-sugars to what the Try!() macro was which is pretty much the match code you wrote. That being said, as I stated above, I don't think the comparison is valid as the benefit of ? over Try! is not about saving characters, but, about permitting method chaining/functional-monadic style with fallibility.

1 Like

Sorry if I hurted you in any way. I didn’t mean it.

@gbutler

Okay, I buy this argument. However, I don’t like the way they propose throw/bail/fail/… See some thoughts above.

try!(...) is a good candidate because you’re likely to chain them and have profit from not having extra braces and so on while you never want to chain throws

2 Likes

This is precisely my position as well, and one of many reasons I've argued against try fn, "catching functions", or anything else that tries to hide the fact that error handling in Rust is just a simple Result return value.

I actually think there's a common element here between a few proposals I've argued strongly against. The match_default_bindings change to pattern matching, which made it so you could match a &Option<T> against Some(x) and get an x of type &T without writing an & in the pattern has precisely the same property: it started from the premise that the & is noise, rather than signal.

& isn't noise. Ok and Err aren't noise. They're signal. Deleting them makes code harder to read. I want the compiler to tell me when I fail to include them, so that I have to change my code to include them.

22 Likes

@vorner, your entire post (the one I’m replying to here) reflects the same sentiment I’ve felt about several changes and proposed changes to Rust, as well. I feel like there’s a fundamental difference in design philosophy at work here.

These are not small changes; they’re changes that will have a sweeping effect on the Rust ecosystem. Anyone reading Rust code will need to take them into account, and cope with reading code that uses them. And that’s leaving aside whether the Rust toolchain itself will steer people to use them.

15 Likes

I really like 2 rules from python:

  1. Explicit is better than implicit
  2. There should be only one obvious way to do a thing

This suggestion breaks both for very little benefit.

I can add more rules that apply here: Special cases aren’t special enough to break the rules. Simple is better than complex.

It seems this is adding complexity to solve subset of some special cases at the cost of significantly changing the language.

9 Likes

In general, this seems like a good idea, but I'm afraid there's a bias here. It's much easier to write a blog post „If we made this change, it would have these advantages and these disadvantages of the current system would go away and how great it would look like“ than „You know, there are some little paper cuts, but overall what we have now seems like the best thing in the whole industry“. Part of it is that we know the disadvantages of the current system, but only guess at the ones the new one could bring and people tend to be optimistic about this. It's a variation of the classical „Nobody will ever give you as a good thing as I promise you“.

However, I feel there's a bigger problem or bigger form of bias here. I remember a very similar proposal to have surfaced already several times. Every time, it got quite a loud opposition, heated discussion, bad air. In other words the very opposite of „consensus“. It was deemed „controversial“ and it died. It wasn't about bikeshedding the right name for something, but about a deep conceptual disagreement with the change as a whole. Yet we see it appear again and again, to have the discussion again and again. This sucks energy of all sides. Are the authors just hoping it'll get through this time? Or that they know better than half of the community (based on the, arguably small, statistical sample of people actually discussing the issue)?

Because if one side wants to keep the status quo (maybe with some minor tweaks), it has to defend it every single time. If other side wants a change, it needs to get through just once and there's no reasonable way back, due to stability promises. Is there a negative RFC template in a form „We don't want this and let's not discuss it again unless one of these conditions change“ or something? The C++ is a pretty good case study in the sense that what is not included in the language is often more important than what is. The standardization committee are very smart people, yet they repeatedly add more bloat to the language. I'm afraid it is some kind of Ivory Tower problem there.

Yet there's no real way to remove things from the language and I haven't seen a process to decide on a feature that it is unwanted (there are some features that the folklore knows are unwanted, but to my knowledge, these are not written anywhere). Are we doomed to end up with C++ multi-tentacle monster due to how the RFC process works, only much faster, because Rust has more agile process?

Sorry if I sound harsh a bit at places. This isn't supposed to be personal, it's a professional disagreement, and more with the „crowd mind“ than with any particular person. If I wrote something that feels personal please understand I had a very bad sleep last night due to how this thread made me sad, so my social feeling might not work as it should. Still, I felt it is better to write this than to keep it for myself.

30 Likes

I don't think this is the right way to look at it. break doesn't actually need to do any wrapping, conceptually. The block evaluates to the success type T, and break represents early return from that block. try then wraps the value in a type which can also represent the failure case.

Edit: Of course, the difference then is that break-from-try is an inner break, while break-from-loop is an outer break, i.e.

'loop: { loop { break 'loop <value>; } }

vs

try { 'try: { break 'try <value>; } }
1 Like

@Centril @newpavlov Unfortunately what both of you propose, one or another way, is an introduction of yet another syntax form for a more or less stabilized feature.

@newpavlov
Introduction of try fn. As someone mentioned above, there’s a much more natural form fn x(...) -> R throws E which, as I understand, was already proposed. try fn only introduces different function declaration syntax just to have some symbols shaved-off, return Ok(...) -> return ... and Err(...)? -> fail .... Yes, I read initial post, with at least two more traits added to stdlib. Is it worth? I doubt so.

@Centril
Your master plan effectively includes

  1. Introduce auto-wrapping based on function declaration.
  2. Introduce different meaning for break keyword
  3. Introduce auto-wrapping dichotomy, where fail always wraps as Err, but return and break don’t.

You intend on introducing a bunch of corner cases just to transform

fn foo(arg: usize) -> Result<usize, usize> {
  if condition(...) { Ok(arg + 1) } else { Err(42)? }
}

into

try fn foo(arg: usize) -> Result<usize, usize> {
  if condition(...) { break arg + 1 } else { fail 42 }
}

Please consider amount of cognitive load to learn both normal path and your path, with all of their variations.

If you ask me, I would look towards something like this:

fn foo(arg: usize) -> usize throws usize {
  if condition(...) { arg + 1 } else { throw 42 }
}

Because it explicitly separates success and error types. The problem with it - hiding of the fact Result is used under the hood.

2 Likes

Well, why not try writing “negative RFC” titled like “Decline attempts to introduce exception-like syntax or semantics, without strong argumentation and/or major support from community”? Unfortunately I’m usually too lazy :slight_smile:

This is an incredibly small sample of the community, especially when the same tiny group seems to monopolize these conversations every time they come up.

I understand that it feels like you have to keep making the same point over and over within the same thread, but I don't think that's necessary. Make your point once, bring up the tradeoffs you see, and let the other side of the argument do the same. There will be upsides to proposals you don't like, even if you don't care about them. If the process doesn't advance (as it notably has not in this case), you don't need to make your point again!

Many times, the language team hasn't had a chance to even read the thread before it spirals out of control like this one, because every little bit of discussion makes you feel like you're losing the fight. Those that do argue for the proposal you hate often don't have a strong opinion one way or the other yet---they may bring up counterpoints just to have them on the table, or to explore the design space. And, you should note, they often do wind up agreeing with you! See @josh's posts upthread, for example.

I hate to make this thread even noiser, but from where I stand it really looks like we need to put more trust in the process. The process is not a vote, the process is not a popularity contest, the process is not a shouting match. The process is a Request for Comments---i.e. a call for the community to describe how a proposal interacts with their use cases. Can we all leave it at that instead of feeling the need to shout down every single comment that we disagree with?

13 Likes

I’ve really enjoyed reading this thread, as long as it’s become, so I’d first like to thank everyone that’s contributed. I can say that as the thread evolved, I’ve liked what’s being proposed more so than the original proposal. But to add my $0.02, I think scaling back the proposal somewhat would hit some of my pain points without changing the language too radically.

What I like from what’s being proposed:

  • A fail keyword that wraps
  • Adding a trait or traits to make any keywords introduced can apply to user-defined types
  • A try block that resolves to an impl of that trait

What I’m less comfortable with:

  • Annotating fn with try…I feel that a fn that returns a Result or impl of the new trait is a natural try boundary
  • Overloading the meaning of break
  • The proposals for Ok() wrapping. Of the terms proposed, pass feels the closest. I could also see ‘resolve’ working, but maybe that’s just due to working with JS promises recently. But I’d almost rather have the asymmetry of only having a keyword for Err() wrapping.

The main hope, I guess, is that improving error handling is an evolution rather than a revolution. Making smaller, limited changes that aren’t too jarring seems preferable to trying to come up with a complete overhaul all at once.

3 Likes

Let me add a vote of qualified disapproval for the proposal, or rather one of strong “meh”. I’ll expand on my reasoning below, but the TL;DR is that while I see the point of autowrapping, I’m not convinced that, in the proposed form, it’s worth the complications that the change would bring to the language.

Up front, I must say that I don’t automatically support appeals to explicitness. I recognize that people have passionate convictions about it, enough that the phenomenon has already resulted in at least two in-depth discussions. I’m not going to repeat the arguments here, just mention that a) Rust already has a fair amount of magic in the form of autoref, autoderef, reborrowing, lifetime elision, closure capture inference, ?-induced error conversion, etc., and b) the introduction of some of those mechanisms elicited its share of dire warnings about loss of explicitness, among others. I wasn’t around for lifetime elision discussions, but I did follow the ? saga from the beginning.

That said, it’s very helpful to be able to reason about some properties of a program without a lot of indirection. I don’t think I’ve ever had any doubts about heap allocation or dynamic dispatch, for instance. (I don’t mean to imply that either of them is bad per se, far from it, just that one should be aware of them in certain contexts.) I’d prefer to keep fallibility of a function in the list of immediately obvious characteristics, as well, and to its credit the proposal under discussion doesn’t try to hide the Result (or any other) return type.

However, the principal downside of the proposal as I see it, especially in its updated form, First, it’s Result-centric to the detriment of other wrapper types. Presumably, if used with Option, you’d have things like fail None. A bit unfortunate IMO. Second, is that the scope of changes is not small—two new keywords and the re-purposing of an existing construct (break).

(Edit: for Option, None is just fail;, which is OK.)

In an earlier topic, I tried to find out some numbers behind certain elements of that proposal. My source and methodology may not have been unimpeachable, but I consider them more informative than pure speculation. While the “editing distance” incidence wasn’t too impressive, the use of Ok(()) was found to be pervasive. I do consider it a mild irritant myself, but I think I’ll happily continue using it, rather than have a wrapping solution which brings too many other complications.

5 Likes

I think that is incorrect; it would simply be fail;

1 Like

Right; I’ve edited the post.

1 Like

This seems like how it should work, but it relies on a coercion from () to NoneError which doesn't exist.

Edit: I guess the proposal assumes Option<T>: TryThrow<()> or whatever anyways.

This assumes that it would be based on () which is not necessarily how it would work. The design of Try, NoneError and all of this are very much in flux, so such assumptions shouldn't be made.

See The desugaring of throw; in the throw RFC for a discussion.

1 Like

Could this feature be implemented a library-based decorator instead of a language feature?

#[try]
fn foo() -> Result<usize, ()> { 1 }

IMHO the decorator approach makes it clearer that #[try] is just an implementation detail. Furthermore it allows a function header in an impl-block to look the same as the function header in the corresponding trait declaration.

4 Likes

I like this solution a lot! I think this discussion has proved that this an controversial topic. This way the people who want to can use the crate without changing the public api.