Pre-RFC: Catching Functions

This is well put.

3 Likes

Iā€™m worried about pass/fail as a proliferation of completely new control flow statements that behave only slightly differently from existing ones and yet have a relatively narrow use case. Is that really preferable to a function-level transformation of return values, even one that has precedent with things like C# Task<T>/IEnumerable<T>, as well as the Python and Javascript equivalents?

(Saying this as someone who is also skeptical of return wrapping, but for different reasons.)

4 Likes

I think this distinction was missing for me. Thanks! I think could get behind either one, but I prefer the marker at each return site (which happens to be the status quo).


More generally, I think one other problem with function-level catch that has not been mentioned yet TMK is that no other annotation on a function/method signature has purely local effects. Everything else is important info that a caller would want to know, but catch is something that could be completely omitted from the rustdoc and nobody would notice. That bother me a little. I'm not sure how important it is, but it might be confusing in its own right...

Hello

I have mixed feelings about the proposal. I kind of agree with throw (and I can see where it helps). I never minded the Ok(ā€¦) thing, maybe just with Ok(()), which is more or less OK, but seems heavy-handed.

However, I donā€™t like the -> T catch E nor -> catch Result<T, E>, for these reasons:

  • First, the use of catch seems really inside out, compared to how other languages use it. Even when I know what it is supposed to mean, the brain still struggles against that meaning and kind of insists on thinking that the function stops these exceptions from falling out, while the exact opposite is true. This is also true for the catch block thing, it would seem to me it prevents that exception from escaping. I guess thisā€™ll lead to huge confusions.
  • Second, the whole syntax seems really adhoc. I enjoy Rust being consistent, very few exceptions in the syntax, the Result isnā€™t really special in the language. This changes semantics in a whole block of code. It is harder to teach if we have to explain ā€žBUT if you are inside a catch block or catch function, it acts slightly differentlyā€œ. This seems like a huge exception to how everything works (the throw doesnā€™t ā€’ if it can be done with a macro, then it can be done with a keywoard ā€’ Iā€™m still not sure wasting a valuable thing like a keyword to save on few characters is worth it, but it doesnā€™t seem actively hurting anything). Does any other language have such thing that completely change the meaning of code this way? Maybe except for Perl6 that can just switch its own parser whereever the programmer wants (but it is consistent in that you can do it everywhere and for whatever reason you like).
  • The catch pollutes the function signature while, in reality, it changes only how the inside of the function is written. Thereā€™s no difference for the caller between -> Result<T, E> and -> catch Result<T, E> function. This just leaks information the caller is not interested in and leads to confusion.
  • The whole ā€žtwo very distinct ways to write the very same thingā€œ approach seems to favour ease of writing over ease of reading. But usually, code is read more times than written. Therefore, the ā€žediting distanceā€œ doesnā€™t seem very convincing argument to me.

All in all, Iā€™d still probably prefer just introducing succeed! and fail! macros, due to much smaller impact into the language. Or even keywords. But this seems like very heavy machinery for such small thing (Iā€™m not trying to say error handling is unimportant, Iā€™m just trying to stress out how heavy-weight this proposal seems).

6 Likes

I believe this isn't what you're asking about but the From::from bit confuses me. Errors get an implicit Into conversion on ? because it's very common to combine different error types, but AFAIK there's no precedent and no need for doing the same to Ok contents. Have I missed something?

1 Like

I think closures can just use a raw catch block-expression, right?

|x| catch { v.get(x)?.to_string() }

Isn't this basically @rpjohnst's proposal but with wrap instead of catch :stuck_out_tongue:

I like the functional-=, but I think it is possibly too much for this RFC. It does solve function-level catch being magic though...


I use vim. I think we should not really assume an IDE when making critical language design choices because it will alienate a large portion of the user base.


I like wrap, but fail also wraps a value...

Patterns for arguments (most commonly mut x: Foo but all irrefutable patterns can be used) are also like this.

3 Likes

That's how ? was introduced (as try!), now that you mention it. It became a language built-in to make it lighter-weight and allow chaining (a()?.b()?.c()?), neither of which apply to throw. Localizing it to an expression with catch was another aspect, which does apply, but since ? already handles that, throw could simply be done with a macro that evaluates to Err(e)?.

What can't be done with a macro is localizing Ok-wrapping return to an expression. For reference, that's very similar to the postponed-due-to-the-impl-period labeled-break-value RFC.

2 Likes

But that's exactly my point though; is this feature actually that critical? And if we can't reach a consensus on it, are the downsides worth getting if most of the upsides can be achieved automatically for a portion of the userbase? As @chriskrycho pointed out, the net win in lines of code to deal with its quite small, and I think we have seen powerful arguments against the change for pedagogical reasons as well. That means the edit distance might be the only argument left that's not contended, but instead of accepting the feature (with its downsides), I'm pointing out that the shorter edit distance might only be useful to those not using an IDE. This makes the tradeoff less worth it to me at least.

If the users who are not using IDEs were in for a world of pain, then putting the onus on the tooling would certainly be a poor solution. But it doesn't really seem that bad; it's certainly not a feature we must have at any cost in my view.

2 Likes

The proposal still felt wrong somehow I havenā€™t described and I couldnā€™t put it into words. But I came with a bit of code that shows why it feels wrong a bit. I admit this is a degenerated example, but these extremes usually show more.

trait Nested { fn ok(&self) -> bool; }
struct Base;
impl Nested for Base {
  fn ok(&self) -> { true }
}
impl<N: Nested> for Result<N, N> {
  fn ok(&self) -> { self.is_ok() }
}

fn do_it() -> impl Nested {
  Err(Base)
}

fn main() {
    if do_it().ok() {
        println!("Can this be true?");
    }
}

Now change do_it with catch:

fn do_it() -> catch impl Nested {
    Err(Base)
}

Suddenly, the whole behaviour of the program changes.

That feels subtle for something as important as error handling. Especially if we are talking about the addition of error handling by minimal edit distance, these things feel brittle. Does that stand to the Rustā€™s aspiration to hack on the code with confidence?

And good luck explaining what just happened there to a beginner.

1 Like

@vorner IIUC, youā€™re trying to say that shorter edit-distance maybe a liability? So if I wanted to start using error handling for a function, it shouldnā€™t be possible to partially make those edits and still compile. Is that what you are getting at?

Yes! And one nice thing about using break from blocks is that is it is already in the language, more or less (now loops), so no need for brand new control flow statements.

The name wrap describes what it really does, without the confounding notions of exception handling from other languages.

1 Like

That's not necessarily a good thing. I think features should be named based on their purpose, not implementation ("what for", not "how"). We have mod, not include_file. break/continue, not goto. pub fn, not add to symbol table.

2 Likes

The effect of a wrap block would be to wrap the evaluated value. Itā€™s in this sense that the name is descriptive. It doesnā€™t say how it is implemented.

Imho, catch is less informative as to the result of the construct. It also carries notions from other languages, that do not give an accurate understanding of its effects in rust.

1 Like

Iā€™ve thought about it a lot and Iā€™m unconvinced that a syntax like pass would actually be an improvement over the current syntax. Itā€™s the same set of rules as today, except instead of writing return Ok($expr) you write pass $expr. This is the same conceptual model, except that you have special syntax. Itā€™s not really ā€œsugar,ā€ since it doesnā€™t abstract any complexity away, its just special syntax.

The advantages i see in the original proposal is not that you use less characters, but that treating the happy path as the normal return path has both pedagogical and ergonomic advantages which I have tried to articulate. Many in this thread have been unconvinced, I know, but thatā€™s where weā€™re at.

8 Likes

@withoutboats I would really like if we could break this RFC up. There are a lot of ideas worth consideration in this proposal and thread, but considering them all at once seems like too much.

For me, I see throw/fail as orthogonal to catching functions. I think having fail is less controversial than catching functions or ā€œhappy pathā€-wrapping. Perhaps this can be its own RFC? I do think it would be a valid addition on its own.

2 Likes

To be clear, I think we should do it with an eye on future compatibility, but I think it really should be done in smaller chunks.

I said in the original post that each subheading can be separated and this is what I meant. This thread isn't an RFC (not even formatted like one) and there's no reason these have to go in together.

1 Like

Ah! Sorry, I missed that. I was under the impression this was intended to be a big pre-RFC :stuck_out_tongue:

I am sort of repeating what you wrote, but: I too feel pretty skeptical about a "pass mode". It feels like it defeats the whole point of this proposal -- put another way, being able to write pass 3 means you have a way to return return Ok(3) that (a) is mildly shorter and (b) doesn't require you to be specific about what the "ok-constructor" is for your particular function, but it still requires you to shift into "another mode".

In my opinion, what makes this proposal valuable is being able to return "normally" -- basically being able to treat error handling as a "side channel" and let the ordinary part of the function be the successful path.

That said, I still feel skeptical of syntaxes that "separate out" Result from the return type. I do agree with the criticisms that -> catch R { ... } feels like an odd syntax, though, and -- for some reason I cannot quite put my finger on -- I don't find -> R catch { .. } particularly elegant.

I was thinking of something. If we changed the (existing) catch syntax to be catch R { ... } and introduced Ok-wrapping, then the following would be a (not particularly ergonomic, nor satisfactory) equivalent to -> catch R { ... }:

fn foo() -> R {
    catch _ {
        ...
    }
}

From here, you can certainly see where the idea of -> catch R { ... } comes as a kind of shorthand.

Have to stew on it.

One other thing to think about: we initially chose catch to avoid confusion with try!. However, it's my impression that try! is pretty well supplanted now with the ? operator. It may be worth revisiting the idea of try { .. } as the syntax for a catch block (and maybe that leads us somewhere at the fn signature level?). My one hesitation is that try R { .. } doesn't seem to make as much sense as catch R { .. } (which reads like "catch the result").

8 Likes