Pre-RFC: flexible `try fn`

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

I don't talk about writing the code, but about reading it. If I see return 42; somewhere, I can deduce the function I'm in returns some int. But with the proposal it can now also return Result<some-int, some-error> or Option<some-int> or something completely different if someone implemented TryOk for something else.

10 Likes

Yes, I think I could agree with that proposal. Because the keywords are „wraps“ without the auto.

6 Likes

I agree with this sentiment. For example, unchecked exceptions (only) in C# were a mistake if you ask me. In fact, I think unchecked exceptions in Java were a mistake. I much prefer the Rust way of panic (equivalent of what unchecked exceptions SHOULD be) and Result<T,E> (equivalent of checked exceptions, but, with a much better way of handling things). I'd hate to see the Rust way morphed into the Java/C# way of things (at least at the surface even if the underpinnings were entirely different).

4 Likes

I view this monadically (up to imperative control flow...):

  • in fn you are in the IO monad
  • in try fn you are in the Try class of monads
  • in async fn you are in the Futures class of monads
  • in const fn you are in the Identity monad.

Personally I find it natural that you can have "wrap value in more structure and early return it". I don't see why it is necessary to deduce that you are in this or that monad when you return expr.

Someone could provide a "law-breaking" implementation of Try; but this holds for any trait... you could also provide a non-sensical implementation of PartialEq and have == be utterly confusing.

I don't see us ever moving into that direction, and I think the semantic underpinnings of errors as values are the vital parts for Rust.

4 Likes

Hm, I think we can work the following compromise: lets introduce try context (i.e. try block and try fn) in which we can use two keywords fail/pass (to be in sync with try, as was said in the RFC throw is in sync with catch) to do autowrapped unhappy and happy returns to the block or function. Tail return will be wrapped as a happy return. In addition we also could forbid uses of return in try fn (to reduce confusion with tail returns). I think it will address some of the concerns listed earlier.

1 Like

May I point out that to many people, monads are the place where they get completely lost while trying to learn haskell and the place where they give up? So introducing some monad-like model into Rust might not really make it easier to grasp?

4 Likes

I think, as someone suggested above, if you rely on the pass/succeed & fail keywords as well as still support return or tail final expression, then "try fn" isn't needed or useful. I think what someone proposed above was:

* let x = try { ... } blocks
* no "try fn" just "fn"
* no "return" from "try" blocks
* pass (or succeed or some similar keyword) permitted in try blocks and in function bodies. Auto-wraps in OK or equivalent.
* fail (or some similar keyword) auto-wraps in Error or equivalent.
* return works like it does today, but, not permitted inside "try" blocks
* tail expression return without semi-colon works as it does today both as final expression of function body and as final expression of try block

This seems pretty good to me. No need to look to the top of the function to see if it was declared as "try fn" vs "fn". Context is completely local. Easy to read/understand. Easy to write. Difficult to see the down-side.

3 Likes

This is often said, but I've explained monads to many people and I've never had any problems with it -- particularly not beginners to programming who use Haskell as their first language. It's a pretty simple concept after all. If anything, there's a built up expectation that monads will be hard, and the jokes about burritos some people make don't particularly help.

We also deal with monads in Rust plenty already (.and_then(..), .flat_map(..), etc.) so it's not particularly new for the language to think in monadic terms (even if the word isn't used...).

Also, to be clear, I'm not proposing that we throw around the word "monad" a lot in official documentation.

3 Likes

For me it starts with try blocks. We want these because we want another boundary for the ?-operator. I don't like the proposed auto-wrapping behavior of try blocks. Repurposing return to return from a try block is also weird. Therefore we can use break instead. Then people complain that break Err()/return Err() is inconsistent. We can fix that by adding fail/pass as keywords. These keyword can also work outside of try-blocks with the same wrapping behavior.

Edit: Also fail/pass make it possible to use loops in try blocks without labels.

Edit2: The try keyword is needed to indicate that it's a boundary for the ?-operator

2 Likes

It does not solve tail wrapping problem, almost always it's used for happy path returns, plus current status quo leads to common use of quite unfortunate Ok(()) quirk.

@MajorBreakfast and then we run into the problem of control-flow keyword proliferation. As I mentioned above, we could perhaps just leave throw/fail and succeed/pass out entirely, just like async leaves out yield.

This would give us try as an effect, very similar to async. They both wrap their values using a trait (Try and Future); they both provide an operator to extract a wrapped value (? and await); they both forbid return in their block forms; they both allow wrapping return in their fn forms.

In both cases, we need no extra throw or yield keyword to open up pandora’s box. There’s just the effect and its operator, and anything else can be done in a library.