Pre-RFC: flexible `try fn`

The thing is, we're arguing about what's right and what's wrong about absolutely subjective things. What's more readable and more quickly scannable to me isn't going to change.

I just don't want to leave every one of these threads wishing I hadn't joined it.

Lints can be disabled, you don’t have to be linted out of anything. I disable some lints myself. My point on linting was in response to the problem of there being many ways to do the same thing – and I proposed a way to solve that; you don’t have to agree with the solution, but it is a way.

They can also become hard errors at some point due to editions. They're also quite a push towards one side.

I'm generally of the opinion that style lints such as that should go into clippy, and not be opt-out parts of Rust.

But that's not what I asked for. I'm specifically talking about each of us accepting that we have differing opinions, and not litigate subjective viewpoints down to every small even more subjective detail. We turn every error handling thread into such a discussion and it's not fruitful.

The language team makes good decisions, but they can only do that because they listen to community feedback. My feedback is that I find the current way things look more readable. And I hope that there'll at least be the compromise of not turning wanting to write Ok and Err into warned behavior.

5 Likes

By discussing things, I'm hoping we can at least understand each other's subjective viewpoints better, and maybe even elevate some points to common facts if we agree.

They could; that's true. But I hope they won't, and given the churn it would cause I am not seeing much benefits to breaking old code in 2021.

Oh! by "lint" I meant a lint inside clippy! So we are in agreement here at least.

Noted :slight_smile:

2 Likes

Certainly.

Excellent, that takes some of the sting out of it. :slight_smile:

Thanks

2 Likes

I must say that I was at first very skeptical about Ok-wrapping. But, after looking at the code examples by @newpavlov above, I think I’m not so skeptical anymore. It seems more ergonomic.

What I’m not so sure about is whether the return should be reused as the keyword for this

  • It should be consistent with try blocks. Many people don’t like to use return to exist from a try block.
  • To check whether Ok-wrapping occurs you need to look at the function signature. That’s not ergonomic.
  • Edit: As @vorner mentions below it makes tail recursion harder/impossible

I propose the following:

  • succeed and fail keywords
  • Both Ok/Err wrapping according to the proposal above
  • Used in normal functions and try blocks. No try functions
  • It’s always obvious by the keyword choice whether wrapping occurs
  • Return retains its current behavior and requires wrapping
  • Open question: How to handle return via expression without semicolon? Possible solution: Don’t handle it, require to spell out succeed ...; instead.
3 Likes

I haven't seen anyone talk about such extreme proposals. As you can see, try fn will be desugared into explicit Ok and Err. The closest thing is @scottmcm's suggestions to restrict ? to try context, but:

  • It will discussed only in preparation to post-2018 epoch.
  • I am not sure if it will find enough support. (I am personally pretty much undecided on this right now)

Originally I suggested something very similar to you proposal, but this questions is the biggest stumbling block. Plus even one keyword is a heavy price to pay, nevertheless two.

I’ll join the „please no“ camp here. Not on particular details, but on the whole direction. Here are the points why I don’t like the proposal (I have bigger problem with the auto-wrapping than with throw, I still believe throw is a mis-nomer, because it hints at exceptions, which is not the case):

  • After all these discussions about error handling, I’m still not persuaded there’s anything that needs fixing deep in the language. Sure, better error types are nice (eg. with ability to downcast). But comparing to any other error handling (exceptions, special-case values (-1), two results), rust’s Result feels the most natural and robust against confusion. Both on the caller and implementer side.
  • Introducing a new keyword is a heavy price for a feature, it makes the language bigger. Introducing a new context is even heavier. Even if there was something to fix, would this really be that important to warrant this kind of artillery? It is already hard to teach Rust (yes, I know what I’m talking about), adding yet another way how to handle errors in there doesn’t help. Explaining „Well, Rust does it differently than “ is generally fine and people take it. Explaining „There are these 2 very differently looking ways, which are basically the same thing under the hood“, people tend to ask why and lose some of the will to learn more, because it sounds crazy.
  • As mentioned, some people find the proposed style more understandable, while others less. But having to read both is much worse than either one, for either side. In other words, if auto-wrapping was the only way, from the beginning, I’d probably oppose adding the explicit style now.
  • I think the editing distance is actually an argument against the feature. If I do such a big change to my function as to make it fallible, I should have to look at all places where it makes a difference, not to miss one. And the editing distance is optimising for easy writing, not reading or debugging.
  • Unlike unsafe and (if I didn’t miss something) async, the proposed try context isn’t strictly extensive. If I take existing code and wrap it inside unsafe, it’ll still compile and do the same. It only unlocks more power. However, try makes the code not compile or even changes the meaning. Having the same code mean something here and something else somewhere else is mentally taxing while reading.
  • Auto-wrapping in complex cases like Option<Option<usize>> or Result<Option<usize>, Option<usize>> will be confusing. The Ok or Some or Err annotation makes the intention explicit and clear.
  • As this is a trait method, the auto-wrapping is a hidden and unmarked conversion. I find that auto-conversions are a bigger source of bugs in C++ programs than null accesses and data races together (usually with less severe consequences, though). Sure, there are some auto-conversions in rust (Deref, trait object coercions…), but these are generally conversions to some type that is kind of view into the real object and the original object still stays unchanged somewhere in the memory. The auto-wrapping has a taste the language does something behind my back and erases some of the trust it gained over the time I use it.
  • There’s no way to express a tail-recursive call (I don’t really talk about the tail-recursive optimisation, only about „just call this function and let it decide“). In other words, with normal function, I can return three things: Err(some_error), Ok(some_value), some_result. Here, I can do either throw some_error, some_value. Note that some_result? is not the same thing. As there’s the ? and implicit auto-wrapping, this runs some arbitrary code, which might in theory be expensive, have side effects, etc.
  • It feels like the proposal tries to make error-handling non-intrusive and out of the way. But to build reliable software, error handling is something that is important and needs the attention. Making it too non-intrusive goes against that, you can forget there’s error handling. This feels in the same general direction as „Lifetimes in Rust are hard to learn at first and annoying. Let’s remove that.“ I enjoy the fact Rust makes me be crystal clear in stating my intentions ‒ make up my mind that I return successfully, in this case. The auto-wrapping is in the „Do what I mean“ direction.
19 Likes

I don't believe async is "strictly extensive" either. Going from x to async { x } changes the type of the expression from T to F: Future<Output=T>, just like try { x } changes it to Result<T, E> (modulo the Try trait).

This isn't really the same thing. The conversions that cause problems like this (in C++ and Rust) are generally coercions- that is, you hand something an expression and if it's the wrong type it will be converted. async and try are totally different- they always do the conversion, which means try is essentially just syntactic sugar for Ok. No new conversions (let alone coercions) take place.

2 Likes

On a procedural note:

If you want this open as an option in our design space, we need to reserve succeed as a keyword (or some other synonym of succeed... please consider in a potential RFC if there's a better word..). If you want to write such an RFC, feel free to use one of my reservation RFCs as a blueprint and change some parts ^,-

We have some time constraints, so such an RFC would be somewhat urgent right now.

fail is already being reserved (in PFCP to merge).

1 Like

Adding an additional behavior to return is IMO a bigger price to pay.

4 Likes

It changes the outer shell of the block, but not the code inside. Basically, if I look into the middle of a really long async block or unsafe block, and I don't see the async itself, the constructs in there still will do what I think they do even if I don't scroll up to check if by any accident the whole function might be annotated. The return type is not such a big problem, because the async and the type are both together inside the function signature, or together with whatever variable I assign the result of the block into.

Isn't it the same here? The trait is generic over what it takes and runs an arbitrary function. Maybe it'll be just wrapping in Ok in the case of Result. But I'm afraid someone will implement their own types that do something different, just as people use operator <sometype> in C++.

2 Likes

I mean, the same is true of try? Even with a return inside a try fn, it's not so much the return that's doing the wrapping but the fn itself.

It's still purely a wrapper. That arbitrary function will always run, not just when the types don't match. (For the record I would also not be happy with people abusing this mechanism to do more than just Ok(x)).

This is referring to Ok-wrapping in the tail-position a la:

try fn foo() -> Result<usize, ()> { 1 }

?

The fact the function will always run does not mean the correct flavour of the function will run if I confuse the types. This one is a bit silly and explicit, but I hope this shows there can be a subtle less obvious variant of this:

impl TryOk<T: Any> for Box<Any> {
  // The obvious implementation goes here.
}
1 Like

This argument convinces me, though, initially I was with the return Ok(expr) and return Error(expr) camp as far as not obscuring happy vs unhappy path.

1 Like

You could also do something weird with Deref.

I think @vorner's point is that you need to look at the signature if the try keyword is there. Having to look at the signature to see whether the function in question is a try fn and does wrapping isn't good.

Yes. I propose to make every function just work with succeed/fail. Having to end the function with succeed ...; isn't really a loss because today you need to write return Ok(...);. Though, for consistence it should be required in try blocks as well.

2 Likes

I agree, "throw" would be a terrible choice. "fail" or some keyword like that would be preferable. I like the succeed/fail that is proposed by someone above where succeed automagically wraps in OK, fail automagically wraps in Error (or equivalent) and return does as it does today without need for "try" on fn. Succeed/Fail work the same both at the function and try-block level.

4 Likes

At that point you may as well just not have try fns. Their primary benefit is the Ok-wrapping- ? already stops at function boundaries.

You can also just wrap the entire function body in Ok( ) and have the same problem. Rust is expression-based, we've already gone far, far down that road.

Honestly I would rather just not have value conversion for the success case- there's really no need for it the way there is for the error case.

1 Like

Yes, I could, but nobody suggests I should by introducing a whole language feature for that case and possibly linting to do that. The fact that I could doesn't make it necessarily a good idea. And return something in the middle would still act the same and I'd see the closing ) at the end.

7 Likes

I still don’t see how the change in behavior for return is an issue (assuming no value conversion). It’s not a coercion, it’s just another layer. If you try to write return Ok(...) you’ll get a type error, just like trying to write return ... in a non-try function.

1 Like

This would be confusing, but, I think the proposal that someone made to use 3 different keywords: return (works like today), succeed (auto-wraps), fail (auto-wraps) would keep this from being confusing.

2 Likes