rust-lang / rust

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

Tracking issue for `?` operator and `try` blocks (RFC 243, `question_mark` & `try_blocks` features) #31436

Open nikomatsakis opened 8 years ago

nikomatsakis commented 8 years ago

Tracking issue for rust-lang/rfcs#243 and rust-lang/rfcs#1859.

Implementation concerns:

nikomatsakis commented 8 years ago

Just to be clear, this FCP is about the question_mark feature, not catch, correct? The title of this issue implies the tracking of both of those features in this one issue.

Yes, just the question_mark feature.

Ericson2314 commented 8 years ago

Following up on https://github.com/rust-lang/rfcs/issues/1718#issuecomment-241764523 . I had thought the current behavior ? could be generalized into "bind current continuation", but the map_err(From::from) part of ? makes it slightly more than just bind :/. If we add a From instance for result altogether, then I suppose the semantics could be v? => from(v.bind(current-continuation)).

nikomatsakis commented 8 years ago

There has been a lot of discussion about the relative merits of ? in this internals thread:

https://internals.rust-lang.org/t/the-operator-will-be-harmful-to-rust/3882/

I don't have time to do a deep summary just now. My recollection is that the comments are focused on the question of whether ? is sufficiently visible to draw attention to errors, but I'm probably overlooking other facets of the discussion. If someone else has time to summarize, that would be great!

conradkleinespel commented 8 years ago

I haven't commented before and this maybe way too late, but I find the ? operator confusing as well, if it is used as a hidden return statement, as @hauleth pointed out in the discussion you have linked @nikomatsakis.

With try!, it was obvious that there might be a return somewhere, because a macro can do that. With the ? as a hidden return, we would have 3 ways to return values from a function:

I do like this though, as @CryZe said:

This way it's familiar to everyone, it pipes down the error to the end, where you can handle it, and there's no implicit returns. So it could roughly look like this:

let a = try!(x?.y?.z);

That helps both make the code more concise and doesn't hide a return. And it's familiar from other languages such as coffeescript.

est31 commented 8 years ago

How would making ? resolve on the expression level instead of the function level affect futures? For all other use cases it seems okay for me.

hauleth commented 8 years ago

I do like this though, as @CryZe said:

This way it's familiar to everyone, it pipes down the error to the end, where you can handle it, and there's no implicit returns. So it could roughly look like this:

let a = try!(x?.y?.z);

I have postulated this. I think it would be perfect solution.

birkenfeld commented 8 years ago

With try!, it was obvious that there might be a return somewhere, because a macro can do that.

It is only obvious because you're familiar with how macros work in Rust. Which is going to be the exact same once ? is stable, widespread and explained in every intro to Rust.

steveklabnik commented 8 years ago

@conradkleinespel

we would have 3 ways to return values from a function:

  • implicit return

Rust doesn't have "implicit returns", it has expressions that evaluate to a value. This is an important difference.

if foo {
    5
}

7

If Rust had "implicit return", this code would compile. But it doesn't, you need return 5.

steveklabnik commented 8 years ago

Which is going to be the exact same once ? is stable, widespread and explained in every intro to Rust.

For an example of what that might look like, https://github.com/rust-lang/book/pull/134

hauleth commented 8 years ago

With try!, it was obvious that there might be a return somewhere, because a macro can do that. It is only obvious because you're familiar with how macros work in Rust. Which is going to be the exact same once ? is stable, widespread and explained in every intro to Rust.

In any language that I am aware of "macros" mean "here be dragons" and that there anything can happen. So I would rephrase that to "because you're familiar with how macros work", without "in Rust" part.

kennytm commented 8 years ago

@hauleth

let a = try!(x?.y?.z);

I have postulated this. I think it would be perfect solution.

I strongly disagree. As you will get a magic symbol that only works in try! and not outside.

hauleth commented 8 years ago

@hauleth

let a = try!(x?.y?.z); I have postulated this. I think it would be perfect solution. I strongly disagree. As you will get a magic symbol that only works in try! and not outside.

I haven't said that ? should work only in try!. What I was saying is that ? should work like pipe operator that would push data down the stream and return error as soon as it occurred. try! would not be needed in that case, but could be used in the same context as it is used now.

conradkleinespel commented 8 years ago

@steveklabnik I think of Rust as a language having implicit return, In your example, 5 was not returned implicitly, but lets take this:

fn test() -> i32 {
    5
}

Here, 5 is implicitly returned, isn't it ? As opposed to return 5; you would need in your example. This makes for 2 different ways to return a value. Which I find somewhat confusing about Rust. Adding a third would not help IMO.

steveklabnik commented 8 years ago

It is not. It's the result of an expression, specifically, the function body. "implicit return" implies that you can somehow implicitly return from anywhere, but that's not true. No other expression-based language calls this an "implicit return", as that would be my code sample above.

conradkleinespel commented 8 years ago

@steveklabnik Alright, thanks for taking the time to explain this :+1:

steveklabnik commented 8 years ago

It's all good! I can totally see where you're coming from, it's just two different things that people use in a wrong way often. I have seen people assume "implicit return" means that you can just leave the ; off anywhere in source to return.... that would be very bad :smile:

CryZe commented 8 years ago

@hauleth The ? operator would just be syntactic sugar for and_then in that case. That way you could use it in a lot more cases and it wouldn't have to be a hard to miss return. This is also what all other languages do that have a ? operator. Rust's ? operator in the current implementation would be the exact OPPOSITE of what all other languages do. Also and_then is the functional approach and is encouraged anyway, as it has a clear control flow. So just making ? syntactic sugar for and_then and then keeping the current try! for explicitly "unwrapping and returning", seems to be the much cleaner situation, by making the returns more visible and the ? operator more flexible (by being able to use it in non-return cases like pattern matching).

hauleth commented 8 years ago

Exactly.

Łukasz Niemier lukasz@niemier.pl

Wiadomość napisana przez Christopher Serr notifications@github.com w dniu 02.09.2016, o godz. 21:05:

@hauleth https://github.com/hauleth The ? operator would just be syntactic sugar for and_then in that case. That way you could use it in a lot more cases and it wouldn't have to be a hard to miss return. This is also what all other languages do that have a ? operator. Rust's ? operator in the current implementation would be the exact OPPOSITE of what all other languages do. Also and_then is the functional approach and is encouraged anyway, as it has a clear control flow. So just making ? syntactic sugar for and_then and then keeping the current try! for explicitly "unwrapping and returning", seems to be the much cleaner situation, by making the returns more visible and the ? operator more flexible (by being able to use it in non-return cases like pattern matching).

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/rust-lang/rust/issues/31436#issuecomment-244461722, or mute the thread https://github.com/notifications/unsubscribe-auth/AARzN5-w4EO9_FwNMDpvtYkGUuQKGt-Kks5qmHOHgaJpZM4HUm_-.

CryZe commented 8 years ago

And when working on a Pull Request for the Rust repo I actually had to work with code that used the ? operator and in fact it really hurt readability for me, as it just like ; was super hidden (mentally, cause it's just noise that gets filtered out in the brain) and I overlooked it a lot. And I find that quite scary.

hauleth commented 8 years ago

@steveklabnik we call it "implicit return", because we aren't the only ones.

steveklabnik commented 8 years ago

@hauleth huh, in all of my years of Ruby, I've never heard of anyone calling it implicit return. I still maintain that it's the wrong way to think about it.

brson commented 8 years ago

I've used ? in a few projects and have preferred it to try!, mostly because it is in postfix position. In general, "real" Rust code pretty much enters the Result 'monad' at main and never leaves it except at occasional leaf nodes; and such code is just expected to propagate errors always. For the most part it does not matter which expressions are generating errors - they are all just being sent back up the stack, and I don't want to see that when I'm reading through the main logic of the code.

My main concern with ? is that I could get the same benefit - postfix position - with method macros, if they existed. I have other concerns that perhaps by stabilizing the current formulation we are limiting future expressivity in error handling - the current Result conversion is not sufficient to make error handling in Rust as ergonomic as I'd like; we've already made several design mistakes with Rust error handling that look difficult to fix, and this may be digging us in deeper; though I have no concrete evidence.

mitsuhiko commented 8 years ago

I wrote this many times before but I am absolutely in love with ? and the possibilities of the carrier trait. I converted one project over to using ? entirely and it made many things possible (particularly with regards to chaining) which was too complex with try!. I also for fun went over some other projects to see how they would do with ? and overall I have not encountered any problems with that.

As such I give a huge +1 to stabilizing ? on the basis of a better named Carrier trait which ideally also covers some of the other cases that I brought up in the other discussion about it.

keeperofdakeys commented 8 years ago

My main concern with ? is that I could get the same benefit - postfix position - with method macros, if they existed.

Maybe we need an RFC for this? Most people seem to like the functionality of ?, but not the ? itself.

birkenfeld commented 8 years ago

Maybe we need an RFC for this? Most people seem to like the functionality of ?, but not the ? itself.

There is an RFC with lots of discussion for this. Also, I don't know where you're getting that "most people" from. If it's from the participants in this issue, of course you'll see more people arguing against because stabilizing is already the default action of the team. The ? has been discussed hugely before the RFC was merged, and as a supporter it's kind of tiring to have to do the same thing when stabilization is discussed.

Anyway, I'll put in my +1 for @mitsuhiko's sentiments here.

keeperofdakeys commented 8 years ago

There is an RFC with lots of discussion for this. Also, I don't know where you're getting that "most people" from. If it's from the participants in this issue, of course you'll see more people arguing against because stabilizing is already the default action of the team.

Sorry, my comment was too brief. I was referring to creating an RFC for some kind of "method macros", for example func1().try!().func2().try!() (As far as I know, this isn't currently possible).

Personally I do like the ? operator, but I share the same concerns as @brson, and I think it it would be good to explore alternatives before we stabilise this feature. Including the RFC conversion, this thread, and the internals thread that @nikomatsakis linked, there is definitely still some contention about this feature, even if it is the same arguments over and over again. However if there are no viable alternatives, stabilising does make the most sense.

mcpherrinm commented 8 years ago

It seems premature to stabilize a feature without having it fully implemented -- in this case, the catch { .. } expression.

I have expressed my concerns over this feature before, and I still believe it's a bad idea. I think having a postfix conditional return operator is unlike anything in any other programming language, and is pushing Rust past its already stretched complexity budget.

eddyb commented 8 years ago

@mcpherrinm Other languages have instead hidden unwinding paths at every call for error handling, would you call operator() an "conditional return operator"?

As for the complexity budget, it's only syntactically different from try!, at least the part that you're complaining about. Is the argument against try!-heavy code, which ? only makes more readable? If so, then I'd agree if there a serious alternative other than "don't have any error propagation automation at all".

est31 commented 8 years ago

Suggesting a compromise: https://github.com/rust-lang/rfcs/pull/1737

It may have no chances to get accepted, but I'm trying anyway.

Limeth commented 8 years ago

I like @keeperofdakeys's idea about "method macros". I don't think the ? syntax should be accepted for the same reason the ternary operator is not in rust -- readability. The ? itself doesn't say anything. Instead, I would much rather see the ability to generalize ?'s behavior with the "method macros".

a.some_macro!(b);
// could be syntax sugar for
some_macro!(a, b); 
a.try!();
// could be syntax sugar for
try!(a); 

This way, it would be clear what the behavior is, and it allows for easy chaining.

WiSaGaN commented 8 years ago

Method macro like result.try!() seems to be a more generic improvement to language ergonomics and feels less ad-hoc than a new ? operator.

nikomatsakis commented 8 years ago

@brson

I have other concerns that perhaps by stabilizing the current formulation we are limiting future expressivity in error handling - the current Result conversion is not sufficient to make error handling in Rust as ergonomic as I'd like

This is an interesting point. It'd be worth spending some focused time on this (perhaps you and I can chat a bit). I agree we could do better here. The proposed design for a Carrier trait (see https://github.com/rust-lang/rfcs/issues/1718) may help here, particularly if combined with specialization, since it makes things more flexible.

withoutboats commented 8 years ago

I really doubt that method macros would be a good extension to the language.

macro_rules! macros are currently declared in an analogous way to free functions, and will become even more analogous when the new import system is adopted for them. What I mean is that they are declared like top level items and invoked like top level items, and soon they will also be imported like top level items.

This is not how methods work. Methods have these properties:

  1. Cannot be declared in a module scope, but must be declared within an impl block.
  2. Are imported with the type/trait the impl block is associated with, rather than imported directly.
  3. Are dispatched on the basis of their receiver type, rather than being dispatched based on being an single, unambiguous symbol in this scope.

Because macros are expanded before typechecking, none of these properties could be true of macros using the method syntax as far as I can tell. Of course we could just have macros that use method syntax but are dispatched and imported the same way as 'free' macros, but I think the disparity would make that a very confusing feature.

For these reasons I don't think its a good choice to delay ? on the belief that "method macros" may someday appear.

Moreover, I think there is a line at which some construct is so widely used and important that it should be promoted from macros to sugar. for loops are a good example. The ? behavior is integral to Rust's error handling story, and I think it is appropriate for it to be first class sugar instead of a macro.

Stebalien commented 8 years ago

@hauleth, @CryZe

To respond to those suggesting that ? should be an and_then operator, this works well in languages like Kotlin (I'm not familiar with coffeescript) due to their extensive use of extension functions but it's not so straightforward in rust. Basically, most uses of and_then are not maybe_i.and_then(|i| i.foo()), they're maybe_i.and_then(|i| Foo::foo(i)) The former could be expressed as maybe_i?.foo() but the latter can't. One could say that Foo::foo(maybe_i?, maybe_j?) turns into maybe_i.and_then(|i| maybe_j.and_then(|j| Foo::foo(i, j))) but this feels even more confusing than just saying that rust early returns on hitting the first ? that evaluates to an error. However, this would arguably be more powerful.

eddyb commented 8 years ago

@Stebalien In the accepted RFC, catch { Foo::foo(maybe_i?, maybe_j?) } does what you want.

Stebalien commented 8 years ago

@eddyb Good point. I guess I can leave off the "However, this would arguably be more powerful". It comes down to implicit catch/explicit try versus explicit catch/implicit try:

let x: i32 = try Foo::foo(a?.b?.c()?));
let y: Result<i32, _> = Foo::foo(a?.b?.c()?);

Versus:

let x: i32 = Foo::foo(a?.b?.c()?);
let y: Result<i32, _> = catch  Foo::foo(a?.b?.c()?);

(modulo syntax)

eddyb commented 8 years ago

@Stebalien Another example: if I wanted to pass Foo to a function bar, with your proposal I'd need to:

bar(Foo::foo(a?.b?.c()?)?)

Is this what you have in mind? Note the extra ?, without it bar would get a Result instead of a Foo.

Stebalien commented 8 years ago

@eddyb Probably. Note: I'm not actually proposing this! I'm arguing that using ? as a pipe-operator isn't particularly useful in rust without some way to handle the Foo::foo(bar?) case.

nrc commented 8 years ago

Just to note that I hate the idea of method macros and I can't think of a language feature I would oppose more strongly. They fuzz the phasing of the compiler, and unless we make really quite fundamental changes to the language there is no way they can exist and have unsurprising behaviour. They are also hard to parse sensibly and almost certainly not backwards compatible.

hauleth commented 8 years ago

@Stebalien, with ? as pipe operator Foo::foo(bar?) would look like this: Foo::foo(try!(bar)) and bar(Foo::foo(a?.b?.c()?)?) (assuming that Foo::foo : fn(Result<_, _>) -> Result<_, _>): bar(try!(Foo::foo(a?.b?.c()?))).

Stebalien commented 8 years ago

@hauleth my point was that Foo::foo(bar?)? is much more common than bar?.foo()? in rust. Therefore, to be useful, ? would have to support this case (or some other feature would have to be introduced). I was postulating a way to do so and showing that that way at least would be messy. The entire point of ? is to be able to avoid writing try!(foo(try!(bar(try!(baz()))))) (2x the parentheses!); it usually isn't possible to re-write this as try!(baz()?.bar()?.foo()).

hauleth commented 8 years ago

But you can always do:

try!(baz().and_then(bar).and_then(foo))

Łukasz Niemier lukasz@niemier.pl

Wiadomość napisana przez Steven Allen notifications@github.com w dniu 05.09.2016, o godz. 15:39:

@hauleth https://github.com/hauleth my point was that Foo::foo(bar?)? is much more common than bar?.foo()? in rust. Therefore, to be useful, ? would have to support this case (or some other feature would have to be introduced). I was postulating a way to do so and showing that that way at least would be messy. The entire point of ? is to be able to avoid writing try!(foo(try!(bar(try!(baz()))))) (2x the parentheses!); it usually isn't possible to re-write this as try!(baz()?.bar()?.foo()).

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/rust-lang/rust/issues/31436#issuecomment-244749275, or mute the thread https://github.com/notifications/unsubscribe-auth/AARzN1Hdk6uk5-SoYawtgAbJUDf_8MsMks5qnBumgaJpZM4HUm_-.

nielsle commented 8 years ago

On a slightly related note, it seems that the ?-feature is mainly used by builders, so we could possibly avoid the need for the ?-feature by providing an easy way to construct builders that wrap a Result<T,E>. I proposed something here, but it may need a little more work.

https://github.com/colin-kiegel/rust-derive-builder/issues/25

keeperofdakeys commented 8 years ago

Thanks for your thoughts on the method macro idea @nrc and @withoutboats, it's good to hear some concrete reasons why they wouldn't work.

withoutboats commented 8 years ago

@nielsle I don't think its accurate to say that ? is "mainly" being used by builders. While builders are an example where I think the advantage of a lightweight, postfix operator really shines through, I prefer ? to try! in every context.

tikue commented 8 years ago

@nielsle regarding futures, I was originally concerned for similar reasons. But, after thinking about it, I think that async / await would supersede any need for ? in that context. They're actually fairly orthogonal: you could do things like (await future)?.bar().

(Maybe it'd be nice to have a suffix operator instead of the keyword await so parens aren't necessary. Or maybe a carefully-tuned precedence would be enough.)

cbreeden commented 7 years ago

I would definitely like to see some documentation written before we stabilize. I looked in the reference and couldn't find any mention. Where should we document this feature?

solson commented 7 years ago

@cbreeden I know @steveklabnik generally avoids documenting unstable features, since there's a chance it will be a waste of time if they are never stabilized. I don't know if we've ever blocked stabilization on documentation writing before.

cbreeden commented 7 years ago

@solson You're right, this was probably not the place to bring it up -- or at least shouldn't be related to stabilization questions. I guess I was just imagining a situation where we could decide on stabilizing a feature, but then also require documentation before being released to stable rustc. There is an RFC related to integrating documentation with feature stabilization and release, so I'll just wait for that process to stabilize (but not without proper documentation first, of course)

rustonaut commented 7 years ago

I think the important part of this RFC is to have something of the right side of an expression which acts like try!, because that makes reading sequential/chained usages of "try" much more readable and to have "catch". Originally I was a 100% supporter to use ? as syntax but recently I stumbled about some (clean!) code already using ? which made me aware that outside from simple examples ? is extremely easy overlooked. Which now makes me believe that using ? as syntax for "the new try" might be a big mistake.

Therefore I propose that it might be a good idea to put up some poll before finalizing it (with notification about it on the Forums) to get a feed-back about using ? or some other symbol(s) as syntax. Optimally with example of usage in a longer function. Note that I am only considering that it might be a good idea to rename ? not to change anything else. The poll could list possible other names which did pop up in the past like ?! (or ??) or just something like "use ? vs. use more than on character".

Btw. not using ? might also satisfy the people which don't like it because it is the same syntax as other languages optional type. And those which want to make it a optional syntax for rust Option types. (Through this are not concerns I share).

Additionally I think with such a poll people normally not taking part in the RFC process could be reached. Normally this might not be needed but try! => ? is a very big change for anyone writing and/or reading rust code.

PS: I put up a gist with the function liked above in different variations ("?","?!","??") through I don't know if there had been more. Also there was a RFC for renaming ? to ?! which was redirected to this discussion.

Sorry, about possible restarting a already long ongoing discussion :smiley_cat: .

(Note that ?? is bad if you still want to introduce ? for Option because expr??? would be ambiguous)