rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
94.82k stars 12.22k forks source link

Tracking Issue for experimental `yeet` expressions (`feature(yeet_expr)`) #96373

Open scottmcm opened 2 years ago

scottmcm commented 2 years ago

This is a tracking issue for experimenting with the "throw expression" idea from RFC#0243. The feature gate for the issue is #![feature(yeet_expr)].

Per the lang process, this cannot go further than experimenting without an approved RFC -- and will certainly not stabilize under the name yeet. Please try this out and give experience reports, but be aware that it may well change drastically or be removed entirely.

Currently the primary purpose of the feature is to ensure that redesigns of the Try trait (#84277) are compatible with potentially doing this in the future, and secondarily to experiment with whether this is worth having as an expression, or whether it should be removed in favour of a pure-library approach.

Because the do yeet syntax is explicitly temporary, do not expect various ecosystem tools to support it, especially not those which have stability promises.

Lang initiative: https://github.com/rust-lang/lang-team/issues/160 Tracking issue for standard library additions: #96374

About tracking issues

Tracking issues are used to record the overall progress of implementation. They are also used as hubs connecting to other relevant issues, e.g., bugs or open design questions. A tracking issue is however not meant for large scale discussion, questions, or bug reports about a feature. Instead, open a dedicated issue for the specific matter and add the relevant feature gate label.

Steps

Unresolved Questions

Implementation history

reza-ebrahimi commented 1 year ago

return Err(...) vs do yeet.

What are exactly the added values here?

andersk commented 1 year ago

@reza-ebrahimi yeet has an implicit .into() conversion that return Err(…) lacks.

fn main() -> Result<(), Box<dyn std::error::Error>> {
    return Err("string message"); // compile error, wrong type
    return Err("string message".into()); // works
    Err("string message")?; // works
    do yeet "string message"; // works
}

It also works with Option, custom types (with feature(try_trait_v2_yeet)), and try blocks (with feature(try_blocks)).

I think Err(…)? is almost equivalent for functions returning Result, except there’s a little semicolon pitfall when the Ok type isn’t ().

fn err_semicolon() -> Result<bool, i32> {
    Err(1)?; // compile error, wrong type
}
fn err_no_semicolon() -> Result<bool, i32> {
    Err(1)? // works
}
fn yeet_semicolon() -> Result<bool, i32> {
    do yeet 1; // works
}
fn yeet_no_semicolon() -> Result<bool, i32> {
    do yeet 1 // works
}
clarfonthey commented 1 year ago

Hmm, Err(1)?; not being valid strikes me as a bug, since it's conceptually equivalent to return Err(1.into()); and a diverging semicolon coerces to !, not ().

Edit: I filed #106357 to track this discussion.

reza-ebrahimi commented 1 year ago

@andersk Sorry for the late reply.

The thing I'm trying to understand is why we need to add a new keyword to language for these kinds of scenarios?

There should be many scenarios of this kind we can find in the Rust language and do we really need to resolve them by adding a new keyword to the language?

Cypher1 commented 1 year ago

@reza-ebrahimi I've been thinking about why 'yeet' is different than other cases. I've tried to give a response here, but am happy to take this conversation elsewhere :)

A Yeet keyword (and functions that support yeet and return to produce Result values) is mitigation for the ergonomics problems of Ok wrapping and constructing errors.

In my experience this is near the top of the ergonomics issues that Rust faces (and it seems many others agree: this keyword has been proposed many times, under multiple names). It will also slightly reduce the confusion of those coming from exception backed languages (i.e. most languages).

As one of the most tractable issues that Rust faces (as it's entirely syntactic sugar) I would love to see this get in!

Personally I'd prefer it was named throw to match other languages, despite the mismatch in implementations (i.e. there's no result/exception stack in Rust). I'd also like a ReturningInto or similar trait so that return can have a less easily misused version of Into just for returning values like Ok and Some but that may be overkill.

jgarvin commented 1 year ago

I think the infix $ operator from Haskell would solve most of ergonomics problem yeet is targeting in a more general way. It would take a function or type constructor on the left hand side and an expression on the right, then feeds the expression into the function, so you could write return Err $ foo instead of return Err(foo). It's also right associative so you can do return Err $ foo $ bar instead of return Err(foo(bar)).

Cypher1 commented 1 year ago

I think the infix $ operator from Haskell would solve most of ergonomics problem yeet is targeting in a more general way. It would take a function or type constructor on the left hand side and an expression on the right, then feeds the expression into the function, so you could write return Err $ foo instead of return Err(foo). It's also right associative so you can do return Err $ foo $ bar instead of return Err(foo(bar)).

That's a solution for a different problem imo, (and is often complained about by new Haskellers).

It only reduces the number of parens (while making it harder to parse (as a human) and requiring work arounds when multiple arguments come into play.

More importantly though, it doesn't provide a short hand for return Err(x).into() or avoid the need for Ok wrapping.

withoutboats commented 1 year ago

(NOT A CONTRIBUTION)

Not mentioned in this thread, but critical for understanding this feature, is that it would be intended to interact with try blocks, which would act as a boundary for ? and "yeet" expressions but not return expressions. The point is to create syntactic sugar for an "error handling" scope inside a function, and while there is already an operation for "forward an error from another function" (which is ?) there is no operation for "raise an error of your own."

Of course the same thing could be achieved with Err(expr)?, but this pattern is rather indirect and unclear. The point is to give users clearer structure.

e.g.

try {
    // Do some kind of fallible IO operation, and fail if it returns false
    if !do_io()? {
        do yeet DoIoFalseError;
    };

    // More code here...
}
appetrosyan commented 10 months ago

Of course the same thing could be achieved with Err(expr)?, but this pattern is rather indirect and unclear. The point is to give users clearer structure.

I would argue that while this pattern is somewhat clearer in zig, it is so because in zig there is no such thing as a macro. If the postfix syntax were stabilised, the same pattern could be achieved with

try {
  if !do_io? {
      DoIoFalseError.yeet!();
  };
}

Also for the record, the monadic error handling actually encourages a different approach:

try { do_io()?.failing(DoIoFalseError)? } 
// Or call it `map_false` if you like. 

where all one really needs is an extension trait and a tiny library in Cargo.toml if this is not included in the standard library (and frankly, I don't think it should be).

dev-ardi commented 6 months ago

We're all fine with things like reborrowing. I don't see how Err(1)? is more indirect and unclear than &*string.

Rust is complex enough as is, the fewer symbols we need for basic things such as error handling that you learn early on the better.

tedliosu commented 4 months ago

Of course the same thing could be achieved with Err(expr)?, but this pattern is rather indirect and unclear. The point is to give users clearer structure.

I would argue that while this pattern is somewhat clearer in zig, it is so because in zig there is no such thing as a macro. If the postfix syntax were stabilised, the same pattern could be achieved with

try {
  if !do_io? {
      DoIoFalseError.yeet!();
  };
}

Also for the record, the monadic error handling actually encourages a different approach:

try { do_io()?.failing(DoIoFalseError)? } 
// Or call it `map_false` if you like. 

where all one really needs is an extension trait and a tiny library in Cargo.toml if this is not included in the standard library (and frankly, I don't think it should be).

Complete newbie to Rust here, but I've done quite my fair share of system programming in C, and am also very tired of the lack of rigorously defined try...catch type structures in relatively simple lower level system languages (not that try...catch should be used pervasively anyway). So what if we did something like (notice the use of the more formal yield keyword):

try {
   if !do_io? {
       DoIoFalseError.yield!();
   };
}

Or perhaps the "monadic error handling" way as mentioned by @appetrosyan in this case would be?:

try { do_io()?.fails_over_to(DoIoFalseError)? } 
  // Or call it `map_false` if you like. 

Like @withoutboats said I wouldn't count my 2 cents as an official contribution, just that I personally believe we need to remind ourselves that just like natural languages, the way we name things in programming languages can take advantage of existing "synonyms" based on natural languages.

I understand reusing the same keyword from another very popular language (Python) in a different way might cause severe confusion for beginners, but unfortunately I am unable to think of a better alternative at the moment. :/

And to address @dev-ardi 's point: yes adding more keywords will complicate the language, but unfortunately as well the nature of such things like FILE I/O is that so many things could go wrong (lock on file un-acquireable, file is encoded in wrong format, etc. etc.) that having a relatively simple try...catch way of doing structured programming may actually be very beneficial for all in the long term. DO NOT get me started on having to include some random error.h header for every single darn possible system error known to humankind, lol.

Sources of inspiration: Python yield keyword Yield signs in real life

P.S. minutes after creating this comment, just saw this stack-overflow post and I gotta say that it seems to me using "yield" for iterators seems to be a rather terrible idea imho.

P.P.S. what if...synonyms for throw...?

try {
   if !do_io? {
       DoIoFalseError.lob!();
   };
}
tedliosu commented 3 months ago

Of course the same thing could be achieved with Err(expr)?, but this pattern is rather indirect and unclear. The point is to give users clearer structure.

I would argue that while this pattern is somewhat clearer in zig, it is so because in zig there is no such thing as a macro. If the postfix syntax were stabilised, the same pattern could be achieved with

try {
  if !do_io? {
      DoIoFalseError.yeet!();
  };
}

Also for the record, the monadic error handling actually encourages a different approach:

try { do_io()?.failing(DoIoFalseError)? } 
// Or call it `map_false` if you like. 

where all one really needs is an extension trait and a tiny library in Cargo.toml if this is not included in the standard library (and frankly, I don't think it should be).

Complete newbie to Rust here, but I've done quite my fair share of system programming in C, and am also very tired of the lack of rigorously defined try...catch type structures in relatively simple lower level system languages (not that try...catch should be used pervasively anyway). So what if we did something like (notice the use of the more formal yield keyword):

try {
   if !do_io? {
       DoIoFalseError.yield!();
   };
}

Or perhaps the "monadic error handling" way as mentioned by @appetrosyan in this case would be?:

try { do_io()?.fails_over_to(DoIoFalseError)? } 
  // Or call it `map_false` if you like. 

Like @withoutboats said I wouldn't count my 2 cents as an official contribution, just that I personally believe we need to remind ourselves that just like natural languages, the way we name things in programming languages can take advantage of existing "synonyms" based on natural languages.

I understand reusing the same keyword from another very popular language (Python) in a different way might cause severe confusion for beginners, but unfortunately I am unable to think of a better alternative at the moment. :/

And to address @dev-ardi 's point: yes adding more keywords will complicate the language, but unfortunately as well the nature of such things like FILE I/O is that so many things could go wrong (lock on file un-acquireable, file is encoded in wrong format, etc. etc.) that having a relatively simple try...catch way of doing structured programming may actually be very beneficial for all in the long term. DO NOT get me started on having to include some random error.h header for every single darn possible system error known to humankind, lol.

Sources of inspiration: Python yield keyword Yield signs in real life

P.S. minutes after creating this comment, just saw this stack-overflow post and I gotta say that it seems to me using "yield" for iterators seems to be a rather terrible idea imho.

P.P.S. what if...synonyms for throw...?

try {
   if !do_io? {
       DoIoFalseError.lob!();
   };
}

OK, I have to apologize here as I have clearly overstepped my bounds here a bit, esp. as I realized after some more thinking that having any extra keywords that needs to be memorized for Rust will in fact likely make the language a bit too complicated especially for beginners, and to be fair half the time I just make up stuff that I end up having to retract later.

HOWEVER, the only reason why I'm not jumping onto the Rust train right now is that it is still not as mature as, say, C++, especially given how much of the software world is dependent on code written in C++ (e.g. .NET, Qt, JVM, etc. etc.), and I believe as Theo from t3.gg especially rightly said during this twitch stream (I sometimes misremember so please forgive me if I mis-paraphrase), it's not always a wise idea to just jump onto the latest new tech for any given project. Theo's argument for not doing that is more related to the idea of maintaining a flexible software stack, but I'm borrowing his argument here (esp. as someone who's dabbled with the likes of CUDA, ROCm, OpenMP, etc.) especially for software used as infrastructure (e.g. the Linux Kernel, Oracle Virtualbox), since rely on something brand new for something that cannot be changed at a moment's notice is never a good idea. Basically, with software used as infrastructure one has to take a lot of precautions around the idea of "moving fast", as "moving too fast" can cause critical system breakage.

NONETHELESS, I don't want to have to dismiss Rust as a viable systems programming candidate, as I really like how its "mutable borrow" philosophy encourages the writing of safe code, but I understand the sentiment behind keeping things as simple as possible. BUT I also hate to see progress on Rust being stalled due to unsettled disagreements about fundamental keywords that may be a necessary part of the programming language. So my question ultimately is:

To solve the issue of having a better version of something like setjmp.h in C due to the lack of try...catch like structures in C (given how Rust is supposed to be a "long term successor" to languages like C++ AND C, correct me if I'm wrong), would it be ultimately better in the long run for both the implementation of Rust AND those who write code using Rust to have something like try...catch, OR to simply have a "setjmp.h" like library which may help to take care of things like exception handling?

Unfortunately my very limited experience in designing programming languages doesn't allow me to participate in this discussion in a truly meaningful way, but again I'm just trying to encourage more discussion around this topic in the hopes that this issue won't be a cause for stalling of the development of Rust itself.

dev-ardi commented 3 months ago

I'm just trying to encourage more discussion around this topic in the hopes that this issue won't be a cause for stalling of the development of Rust itself.

I've read your comments like 3 times and I'm still not sure what point you're trying to raise honestly 😄

tedliosu commented 3 months ago

I'm just trying to encourage more discussion around this topic in the hopes that this issue won't be a cause for stalling of the development of Rust itself.

I've read your comments like 3 times and I'm still not sure what point you're trying to raise honestly 😄

My point is: do we want to keep discussing this yeet-based issue for the next two years again, or is it time to find a solution and move on for productivity's sake? Does that make sense? Hope I'm not being too harsh with this.

FragrantArmpit commented 2 days ago

The argument that yeet would add more complexity makes no sense. If yeet is too complex for you then just don't use it.

I find return Err(...), etc very annoying and I think a new keyword is definitely worth it.

Naming-wise a have a concern: why is it do yeet and not just yeet? Also, I saw a suggestion for throw and I think it would be inappropriate because we throw exceptions and the equivalent of exceptions in Rust are panics.

dev-ardi commented 2 days ago

why is it do yeet and not just yeet?

do is a reserved keyword already and since yeet is definitely not gonna be the final name it makes no sense to reseve it.

FragrantArmpit commented 2 days ago

why is it do yeet and not just yeet?

do is a reserved keyword already and since yeet is definitely not gonna be the final name it makes no sense to reseve it.

What’s blocking us from using yeet as the final name?

appetrosyan commented 2 days ago

Common sense? You wouldn’t call objects thangs.

FragrantArmpit commented 2 days ago

Common sense? You wouldn’t call objects thangs.

I assume you’re implying that thang to object is like yeet to throw. I agree that I would not call throw yeet but this feature is not throw so i would not be calling throw yeet, I would be calling this unnamed feature.

appetrosyan commented 2 days ago

I assume you’re implying that thang to object is like yeet to throw. I agree that I would not call throw yeet but this feature is not throw so i would not be calling throw yeet, I would be calling this unnamed feature.

If we're totally honest, the try block is not the try of Python, so there's no reason why you couldn't call it throw. It might confuse some people, but to be fair, C++ programmers can wrap their head around & as in borrow vs. reference.

Secondly, eject or raise or even hurl could work. Hurl has the advantage of me being able to type it on a steno keyboard.

FragrantArmpit commented 2 days ago

If we're totally honest, the try block is not the try of Python, so there's no reason why you couldn't call it throw

Just because I can call something something doesn’t mean I will. Indeed, we could call the try block the throw block but how appropriate would that be?

appetrosyan commented 1 day ago

Just because I can call something something doesn’t mean I will. Indeed, we could call the try block the throw block but how appropriate would that be?

I don't see the point in arguing. Just give me a lint to catch these expressions, so I can clippy --fix them away.