That could be done with single-expression functions, which would equally apply to other block-like structures:
fn foo() -> Result<T, E> = try { ... }
That could be done with single-expression functions, which would equally apply to other block-like structures:
fn foo() -> Result<T, E> = try { ... }
That handles tail expressions, but not return statements. With T
as ()
, you'd still have to write return Ok(())
instead of just return
, for example.
The problem with this similarity is that suggesting the function body is a try
block doesn't capture the behaviour of return
(as mentioned above). One place the confusion between the two becomes acute is in closures:
let closure = |x| try { ... };
Here, the function body actually is a try block and should behave as such, but people might be forgiven for expecting it to behave the same with respect to return
as a function written as:
fn foo() -> Result<T, E> try { ... }
It creates a subtly wrong intuition. I do agree that we're describing more the function body than the function itself, though. I liked the -> Result<T, E> catches { ... }
variant. I'm not sure that -> Result<T, E> tries { ... }
makes sense, by comparison.
Edit: This is a little off topic, but I think it’s worthwhile considering the effects on related features.
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
.
(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):
?
fails; final expression in the block is implicitly Ok
; return
returns from the function, and is still Result
.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:
Any of those really (or something similar) would be better than catch. Please, please, please avoid using “throw”, “raise”, “catch”, “except”, “finally”, etc.
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
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 catch
↦ try
sound good/practical to people? Some thoughts
try!
means that it was used almost never as an identifier, so the costs of reserving it would be lowr#try!(a)
in new epoch, since one can a?
instead.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…)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.
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.
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.
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.
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:
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
.
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:
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:
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.
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:
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.
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:
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:
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
.
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? I could live with
maybe
, as eventually the puns would be delivered deadpan.
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
.