Bikeshed: Rename `catch` blocks to `fallible` blocks

Well, it's a shift-reduce conflict in the grammar that would need to be resolved. I think in this case we could easily pick it being interpreted as a "try function", since there's never a reason to have a top-level try block in a function -- it just scopes where ? applies, and the default is already to the whole function, which is equivalent to what happens with a top-level try.

1 Like

(EDIT: just realized I went way astray from the original bikeshed topic - sorry about that, and feel free to stop me now. Perhaps there’s another thread I should read more about the decisions around catch.)

It seems misleading to make “catch blocks” and “throwing fns” look very similar. It seems to me like they are really separate, orthogonal-ish ideas that can exist separately (if I’m understanding correctly):

  • “catch block”: short circuits if a ? fails; final expression in the block is implicitly Ok; return returns from the function, and is still Result.
  • “throwing fn”: changes both final expression and returns to be implicitly Ok.
  • throw: throw 4; would be equivalent to Err(4)?; (could be used in either normal fn or throwing fn).

(Although catch block and throwing fn are so similar that it might be bad to have both of these slightly different concepts.)

“catch block”:

type R = Result<i32, u8>;
fn f1(a: R, b: R, c: R) -> R {
    Ok(catch { a? + b? }.unwrap_or(0) + c?)
}
// is equivalent to:
fn f2(a: R, b: R, c: R) -> R {
    Ok(match (a, b) { (Ok(a), Ok(b)) => a + b, _ => 0 } + c?)
}

// catch as the outermost block of a function:
fn h1(a: R, b: R) -> R { catch {
    let ab = a? + b?;
    if ab == 0 { Err(0)?; }
    if ab == 1 { return Ok(1); } // No implicit `Ok`.
    ab // Implicit `Ok`.
} }
// is equivalent to:
fn h2(a: R, b: R) -> R {
    let ab = a? + b?;
    if ab == 0 { Err(0)?; }
    if ab == 1 { return Ok(1); }
    Ok(ab) // Just adds the `Ok` here.
}

“throwing fn”:

fn g1(a: R, b: R, c: R) -> R throws { // or whatever
    let ab = a? + b?;
    if ab == 0 { Err(0)?; } // (Could also be `throw`.)
    if ab == 1 { return 1; } // Implicit `Ok`.
    ab + c? // Implicit `Ok`.
}
// is equivalent to:
fn g2(a: R, b: R, c: R) -> R {
    let ab = a? + b?;
    if ab == 0 { Err(0)?; } // or `return Err(0);`
    if ab == 1 { return Ok(1); } // Adds Ok here.
    Ok(ab + c?) // Adds Ok here.
}

To avoid that confusing tiny difference, perhaps catch {} blocks should be avoided in favor of something that looks and behaves more like the IIFE (immediately-invoked function expression) in this currently-valid Rust:

fn f3(a: R, b: R, c: R) -> R {
    Ok((|| -> R {
        Ok(a? + b?)
    })().unwrap_or(0) + c?)
}

I still say that all this talk of “Catch” and “Throw” is only going to cause unneeded and unnecessary confusion in the Rust ecosystem. Rust doesn’t do catch/throw in any meaningful sense in the way that traditional exception-based languages do that use the “throw/raise/catch/except/finally” type of syntax.

Injecting “catch” and “throw” into the terminology of Rust is only going to cause confusion. Please, please, please, use some other terminology. My order of preference would be:

  • resultof
  • do
  • trap

Any of those really (or something similar) would be better than catch. Please, please, please avoid using “throw”, “raise”, “catch”, “except”, “finally”, etc.

1 Like

Thanks for the extensive response, all! (I should have known for what I was getting myself in by starting a thread with “bikeshed” in the title…)

In particular, thanks @Xanewok for doing a better job of explaining and motivating my idea than I did :heart:

I think overall I was convinced (3, 4, 38) that the unfamiliarity tax of fallible is too high. I definitely like the alternative of try, though.

So does catchtry sound good/practical to people? Some thoughts

  • Hopefully the existence of try! means that it was used almost never as an identifier, so the costs of reserving it would be low
  • And there isn’t a worry about needing r#try!(a) in new epoch, since one can a? instead.
  • On the other hand, would it be confusing that try{} would be the complement, in a way, of the old try!? (It kinda makes me think of how one can add unsafe blocks to call unsafe methods in a non-unsafe function where one couldn’t otherwise, so might be fine…)
19 Likes

I’m against using try, for the “On the other hand, would it be confusing that try{} would be the complement, in a way, of the old try!?” reason. There’s still a lot of code out there using try!, some folks still call ? the “try operator” (I used to!), and it’s there in a lot of examples. I’m very wary of introducing something that’s its direct complement that shares the name.

Unless we manage to purge try! from the collective memories of Rustaceans and beginner Rust materials on the Internet (this includes stack overflow), I don’t see how this won’t be super confusing for newcomers to deal with.

I don’t really like catch either, but I feel that try would be worse.

1 Like

I’ve searched StackOverflow for “[rust] try”. There weren’t many questions/answers that mention it. I’ve edited a few to remove irrelevant try!. The ones left are mainly duplicates of a question about use of try! and ? in main() (which is being fixed).

The second edition of the Rust book and current Rust by Example have already removed all mentions of try!.

It’s possible to make rustfix or rustfmt replace try! with ? automatically.

So overall I think it’s feasible to reduce the try! macro to a historical curiosity to the point it won’t be actively confusing to newbies coming to Rust.

11 Likes

There are still plenty of materials out there which mention try.

This was always going to be a problem with the edition but it’s one thing for some old things to not compile and it’s quite another for them to swap their meaning entirely.

Purging from the “collective memories of Rustaceans and Rust materials” is not something that easy.

Edit: Basically, Rust 2015 is still going to be around, materials written for it will also still be around. Python 2/3 have similar pedagogical problems, I strongly wish to avoid those.

1 Like

I still don’t understand the fixation on reusing “try/catch/throw/raise/finally” type of terminology for this in Rust. There are a number of other proposals given that would avoid the confusion.

4 Likes

Could you say more about the “other proposals” that seem compelling to you?

EDIT: Whoops, didn’t notice you had pre-existing posts in this thread that covered that. Oh well, the rest of this post was stuff I wanted to say anyway.


AFAICT nobody in this thread is advocating catch as the optimal keyword for this feature (the closest we got was one post saying catch is better than try), and the real debate is between:

  • a familiar keyword that has some pre-existing association with error handling, and
  • a brand new unfamiliar keyword that can’t possibly be associated with the error handling of some other language.

What’s interesting is that (if I’m reading the zeitgeist of the thread right), the familiar keyword side seems to mostly be leaning towards try, while the unfamiliar keyword side seems to have not even begun to converge on what the best choice might be. Which means there’s a risk of try winning “by default”. So for that reason alone, I think anyone speaking against familiar keywords like try ought to say a little bit about what unfamiliar keywords they do or don’t like (even if they have nothing more to say than “+1”, “meh”, “-1”). While I currently favor try, I don’t think we’ve explored the unfamiliar keywords anywhere near enough to conclude whether or not try should be the “winner”.

FWIW, my knee-jerk reaction is a roughly-identical “meh” for all of the unfamiliar keywords proposed so far. I think they’re all way better than catch, but all slightly less good than try.

1 Like

As I said above:

My personal preference would be "resultof", but, "do" or "trap" would be good as well. I believe some other people were in favor of "trap" as well. I'd be much happier with "trap" than "catch" or "try". So, my preference would be:

  • resultof
  • trap
  • do
  • ...several other options would probably still be preferable to try/catch
  • try
  • catch

I do agree though, that most of the responses seem to want "try" due to familiarity. That being said, it doesn't seem like this thread has had a lot of participants, so, I'm not sure how indicative that is of the overall community preference one way or the other. I'm just taking the "Bikeshed" title as an opportunity to advocate for what I think would be best. At the end of the day, it all comes down to opinion. There really isn't a right or wrong. Just perception.

EDIT: I see it like this. When you get behind the wheel of a car, you expect the steering wheel will control the direction of the vehicle, the brake pedal will bring you to a stop, the gas pedal will accelerate the vehicle, etc. If I create an auto-drive car that doesn't require the human being to steer (at all), would I then want to repurpose the steering wheel to controlling the direction the camera for the in-dash infotainment system points or would I be better serving the user to provide an entirely different control (like maybe a joystick) instead (yes, I know the analogy is imperfect, like all analogies, but, you get the idea).

I feel like Rust error handling is different enough from exception handling that using the same keywords will only cause confusion by making those new to Rust think that it works like exception handling they've seen in the past. Perhaps that "fear" is unfounded, but, that is my take on it.

I really dislike resultof because of two reasons:

  • It is the concatenation of two words which is a bit ugly in my opinion, I also believe there is no precedent for this in keywords.
  • It only really makes sense for Result and not for other implementers of the Try trait like Option.

That said I am in favor of a new unfamiliar keyword and I feel like trap is a pretty good option.

2 Likes

I believe that because the “Try” trait has already been stabilized, that I’ve already lost this argument. “Try” is forever associated with error handling in Rust. My prediction is that there will be endless pressure to make Rust more and more “exception handling-like” over time as those new to the language don’t understand why Rust “try” is different from every other languages “try…catch” etc. Maybe I’m wrong, but, that seems like the kind of thing that happens (in my experience).

EDIT: To clarify what I mean by “lost the argument”: It seems that “Try” has already been blessed as the keyword to talk about Rust error handling (because of the try! macro and now the Try trait). It’s probably too late to reverse that momentum and my opinion on the matter is actually of little importance at this point due to, if nothing else, the inertia of decisions already made. I will note though, that the idea of “result/resultof” being the keyword still isn’t entirely ruled out by the existence of the “try” trait as you suggest as the “try” trait pretty much is built around the notion of making things other than Result<T,E> be able to have an interface compatible with Result<T,E> as shown here:

pub trait Try {
    type Ok;
    type Error;
    fn into_result(self) -> Result<Self::Ok, Self::Error>;
    fn from_error(v: Self::Error) -> Self;
    fn from_ok(v: Self::Ok) -> Self;
}

It seems like this trait when implemented for an arbitrary type has 2 purposes:

  1. Return an instance of the arbitrary type T when given and OK or ERROR type
  2. Return a Result<T,E> where T corresponds to the OK type and E corresponds to the Error type

So, anything utilizing the Try trait is effectively yield a Result<T,E> even though it isn’t a Result<T,E> itself. So, the notion that “resultof” or “result” would be a bad keyword because “things other than Result<T,E>” actually doesn’t apply (in my mind).

That being said, “result/resultof” definitely doesn’t seem to be gaining any traction, so, I’ll yield the floor to anyone with better ideas.

3 Likes

The Try trait is unstable <https://doc.rust-lang.org/nightly/std/ops/trait.Try.html>, and has not been proposed for stabilization -- on the contrary, the recent discussion on its tracking issue is about dissatisfaction with the current design:

2 Likes

Well, that sort of changes my opinion then. I, for some reason, thought that Try had recently been stabilized. If it hasn't, then I would say that whatever we agree is the appropriate keyword for these "fallible" blocks should be the same name that the "Try" trait should have. If "Try" is pretty much a given for the "Try" trait, then probably just use "try" for the fallible block. If the "Try" trait is still open to renaming, then, I think the community should agree on the appropriate name for both the block and the trait. Again, I like:

  • fallible / Fallible
  • errable / Errable
  • unsure / Unsure
  • resultof / ResultOf
  • uncertain / Uncertain
  • erring / Erring

I'm not sure how I feel about all of the above, but, they all seem better than "Try". I've ordered them above in what I would prefer from most preferred to least preferred (I think). Anyone else think anything like this might be a way forward on this discussion?

Another name possibility is maybe {} / trait Maybe.

3 Likes

I like it!

Though, if memory serves correctly, Maybe is the monad in Haskell that is akin to Option in Rust, is it not? So, maybe (see what I did there) Maybe might not be the right thing either.

IIUC, Option and Result are both monoids. Would that make ? a monoid operator and Try a monoid trait? :smiley: I could live with maybe, as eventually the puns would be delivered deadpan. :wink:

More things using the try_ prefix were FCP'd for stabilization (try_from, try_fold, etc). But that's just continuing the pattern of marking fallible things with that prefix, like Mutex::try_lock.

1 Like

Yes it is (and i saw what yo did here :smiley: ). And i see your problems with using "already" established names for "things" that would impose other semantics in Rust by using it. But i also lean to the sentiment of "It does not matter as long as it leads the user into the right direction although its not a correct substitute in detail".

Your argument against catch is, that it is used in other languages in the context of exceptions with a different semantic. But it is within the domain of error handling. So being opposed to every term that was ever used elsewhere which has a different semantic is very limiting and also not exhaustive (nobody knows every occurrences etc. and where do we draw the line between popular/unpopular if we decide to reuse unpopular ones) which could be a worse trade off of reusing an established term but with a different semantic but within a certain domain. You see, you liked maybe (i do too) but are limiting yourself to not like it because Haskell is using it in a different way semantically. The question here is at which point it does not really matter? try, catch, maybe (to choose some) are good candidates and to be honest i have no problem with catch because even though it is used in many other languages for a different thing semantically it is easy for most of the programmers out there to associate this term with error handling – and that might be enough to reduce mental overload. If i see catch { .. } with a minimal example i claim that more beginners understand AND remember what this means and how to use it opposed to a term like trap etc. which they never saw in any other programming language and their brain is not wired to that term belonging to the domain of error handling. I imagine people sitting in front of the monitor wondering "what was this weird keyword again to stop propagating errors .... lets consult documentation"

TLDR; I see your problem with reusing terminology in semantically different contexts BUT i think it is unnecessarily increasing mental overhead to use terminology not very popular in current programming languages! And i hope your struggle with maybe (which might be a good candidate) is self evident here?

6 Likes

There’s a bunch of stable functions with try_ prefix already, so I think in Rust’s context it already has an established meaning. Change to a different keyword not related to try/catch would have to deal with the dilemma of either duplicating these functions for consistency or still having try/catch-related names and naming inconsistency.

5 Likes