Pre-RFC: Catching Functions

Why is the case with try worse?

Because there's (at least for me) usually a relation of the expression and the return value. If we add things like try, we'll add additional points into that relation.

Plus, the point was about "making sure things don't compile for half-refactored code" concerning error handling.

2 Likes

Of course, but that's not a good argument to me for making it more risky. If you change None to Some(None) you're actually changing logic. If you go from try to non-try or vice-versa, you're trying to not change the logic.

Sure, so do I, but that won't work for combining two traits that both have associated types that have to be combined and are Option<T>.

What do you think about the 23.into() case for Option<i32>?

...and since it's an obscure corner case - is it the lesser evil to ban try on such functions?

While pure has been retracted, nobody seems to have mentioned one of the most fundamental intuitiveness issues it had:

Things like let, match, try!, catch, return, throw, raise, wrap, etc. are consistently verbs. pure is an adjective like public, mutable, or static, which generally implies it to be part of a type or function signature.

Having a consistent ruleset for relating programming language grammar and natural language grammar is the first layer that contributes to intuitiveness.

Also, since that comment could come across as an implicit ā€œI’m in favour… given the right bikesheddingā€, I’ll clarify that, having read the entire thread, I share the fundamental concerns expressed by @chriskrycho and @H2CO3 and the mindset expressed by @ranweiler and @josh

4 Likes

I have to agree with that. Even having contributed to the discussion in several RFCs (eg. analogizing .. and ..= to < and <=), ā€œPre-RFCā€ feels so formal that I still found myself caught up in a ā€œthis is too important to risk waiting for an RFCā€ pseudo-panic and the only thing holding me back was the knowledge that I didn’t really have much to say that hadn’t already been said better by someone else. (Though it did lead me to stay up hours beyond bedtime, religiously liking the replies I agreed with.)

Perhaps just Idea: as the prefix? It’d still be a distinctive string but, at the same time, feels quite informal to me. (More in the vein of ā€œThis came to me while I was on the bus. What does everyone think?ā€)

It would also have the benefit of possibly increasing suggestions, since, not really knowing what the rules are for a ā€œPre-RFCā€, I’ve been operating under the assumption that I’m not even qualified to write one without embarassing myself.

(I have no background in language design and my programming experience has been results-oriented, so I never spent any time in ā€œlanguage theorists’ playgroundā€ languages like Haskell or more than one single-semester beginner’s C/C++ class in ā€œyou’ll eventually either sink or figure out some aspect of the machine modelā€ languages.)

2 Likes

Agreed completely.

2 Likes

Another point to consider regarding implicit early return: while we do want to preserve ? in normal code, implicit early return could be useful in solving problems around ? in closures.

Treating try as an effect, a generic function could be made effect-polymorphic in order to pass through early returns without changing its interface in the infallible case. For example, you might write something like this:

cache.entry("key").or_insert_with(|| try {
    let x = try_calculation()?;
    process(x)?
})

If, as today, or_insert_with had to decide up-front whether its argument had type F: FnOnce() -> T or F: FnOnce() -> Result<T, E>, it could just use ? in its body when calling it. However, if it were instead something like F: ?try FnOnce() -> T, it could implicitly pass through Err returns from its argument without any changes to its body. (Alternatively we could have some kind of ?-only-if-try syntax, but that sounds pretty ugly.)

These kinds of combinators (most are probably iterator adapters) are generally pretty small and don’t do any of their own error handling, so the ability to pick out all the ?s may not be as important there. I also, again, think this would also be really useful for async functions to be able to keep using all these APIs.

(Inspired by my comment here.)

I thought of a couple more questions about the problem that I don’t think we’ve listed:

  1. Do we want to see purely local information (e.g. try) in a function signature? This would break precedent with all existing notation in a function signature.

  2. What are the tradeoffs of rightward shift with try blocks and functions.

For the second question, I think having try functions would mitigate rightward shift, though I personally don’t support the idea.


All of that led me to introspect a bit about try blocks and functions. For me at least, try imposes a mental burden – additional context information that I need to keep track of in addition to the algorithmic context that I actually care about. It’s an extra layer that is between me and the basic types. I can see the usefulness of try blocks for control flow, but only if they are short like some of the examples in this thread so that I don’t have to keep track of the context for more than a few lines. That’s part of what bothers me about try functions, I think. You have to read the entire function with that context in mind…

7 Likes

I stopped following this because I felt like this proposal "was just going to happen" and I'm not active on the forums so I wouldn't have a chance to step out in front of it. I'm very much in the same boat as Josh here. I love Rust for the fact that it makes me write better code and care about the right types. It makes me a better programmer and it helps me write better code. This proposal just feels like its watering that down and says "handling errors properly is annoying. I don't even want to unwrap() each line, I want to just unwrap all in one block. fwiw, I've been writing Rust for almost 2 years and I've contributed to the compiler here and there over the years. I've also maintained the Rust package for a number of distros along the way and advocated the language. I certainly read the RFCs that are out there but like I said I keep quiet but this post made me want to speak up. Not to be harsh but nothing I've encountered to date has turned me off of Rust quite like this proposal has.

8 Likes

Well, it wouldn't be part of the signature. It would be part of the definition, but there's already quite a lot of stuff there that's not part of the signature, notably everything related to the argument patterns: the argument name at all, whether it's mut, whether it's destructuring that argument, ...

Can you clarify exactly what refactoring you're talking about? There's lots of ways to write inference-dependent code that works in a bunch of contexts.

I definitely agree it should be unambiguous. A previous discussion about a feature like this talked about potentially having it as a coercion, but that would allow both return 4 and return None in an -> Option<i32>, which I certainly don't like. And those two cases get even worse in -> Option<Option<T>>...

I just wanted to say thank you to @withoutboats for this proposal. Having tried to champion a new Python stdlib feature that ultimately got shot down, I sympathize that it can be frustrating when you work towards an idea but other people don’t assign the same values to the cost function involved with adding the feature.

I think everyone can agree that proper error handling in any language is very difficult, but the computer science field as a whole has been making steps slowly in the right direction.

Consider C, where return values from something like write(socket, ...) can return either the number of bytes sent, or signal an error that an error has occurred (which can be inspected in another global value, errorno). There’s no way statically to enforce that this error is read, must less handled correctly.

When I left work on Friday, I found that my C++ program had thrown an unhandled out_of_range exception. Since C++ doesn’t include stack traces or have something like ?, I’m in for some serious debugging to find even where that was thrown, much less fix it.

Rust’s Result<>, ?, and panic! are (in my opinion) huge innovations here. They solve very real problems, but of course there is a cost associated with them (as @withoutboats has described).

My point to all of this is that we need proposals like this in order to push the field farther, because I think Rust is at the very forefront of systems programming technologies. They may not always pan out, but we shouldn’t discourage each other from continuing to explore this space. We of course must be conservative and cautious. Another excellent example (from @withoutboats of course) is the failure crate, which clearly innovates in this space.

So in summary, thank you to everyone for the proposal and feedback. Let’s use this feedback to imagine new ways to improve error handling and make history.

10 Likes

(not "him")

3 Likes

Thanks for the info, edited the post.

3 Likes

Sorry, I used incorrect terminology... You're right about args, but somehow it doesn't seem the same to me... I'm not really sure why, though... I think it might be that pretty much all keywords in a definition are relevant to the caller. The exception is mut on args, and I honestly have never liked that either...

1 Like

I mean something like this: Pre-RFC: Catching Functions - #258 by phaylon

Here’s my humorous attempt at predicting the future:

The year is 2020, Rust has been used in production on many high-profile applications. But one thing has been an issue since 2018: There’s no way to ? out of nested catch blocks.

As the user base grows, and a clear need for ? in nested catch blocks emerges, a proposal from 2018 is revived: labeled ?.

When the original proposal supporting ? with any labeled block is deemed controversial, it is then restricted to only work on catch blocks. This quickly gets accepted and stabilized, and all is well. Until…

One year later, result-producing loops become an important construct in Rust. These simple-sounding constructs have the form:

'one_label: catch {
    'other_label: loop {
        ...;
        ... 'one_label?;
        break 'other_label;
    }
}

However, there’s still no clear way to ? out of a function, besides putting a labeled catch block inside the function. To help with this, function definitions are now allowed to take the form:

'label: fn make_me_a_sandwich() -> Result<Sandwich, PrivilegeError> {

Thus allowing exiting the function from nested catch blocks, with 'label?.

Sadly, it’s now 33 years too late, and the only solution seems to be to start over.

2 Likes

I suspect that labeled break from blocks, which looks like it’s going to be merged, will be sufficient. ? will work great in simple situations up through try blocks, and in the rare situations that you truly want wackier control flow, you can just fall back to break-with-value.

And for the function-level case we already have return!

I’m pretty sure we have neither some_file.read_exact(bytes) return? nor some_file.read_exact(bytes) break 'label?. Indeed, if we had them, the whole idea of catch blocks would be redundant, and we wouldn’t be having this conversation.

(I don’t know about you tho, but I still prefer ? and 'label? - no return or break.)

That seems too goto-like for my taste. I don’t like labels at all… Anyway, that’s bikeshed for another day :stuck_out_tongue:

2 Likes

What I’m saying is that we don’t need those because they’d be rare enough that you could just write this instead:

if let e @ Err(_) = some_file.read_exact(bytes) {
    break 'label e;
}