loading...

"try fn" without special-casing Result

cad97 profile image Christopher Durham ・3 min read

There has been much talk recently about "try fn" in Rust. This is to add some fuel to the fire and address one major argument against try-like sugar for functions: the special casing of Result.

This is explicitly not supposed to be a proposal. This is just meant to open discussion on a new avenue of the "try fn" design space I haven't seen mentioned yet.

Quickly summarizing the potential feature (but please, don't focus on the concrete syntax):

try fn read_to_string(path: &Path) -> String, io::Error {
    let mut file = File::open(path)?;
    let mut string = String::with_capacity(initial_buffer_size(&file));
    file.read_to_string(&mut string)?;
    string
}

essentially would desugar to

fn read_to_string<Return>(path: &Path) -> Return
where
    Return: Try<Ok = String, Error = io::Error>,
{
    try {
        let mut file = File::open(path)?;
        let mut string = String::with_capacity(initial_buffer_size(&file));
        file.read_to_string(&mut string)?;
        string
    }
}

except return would be subject to the exact same "ok wrapping" behavior as the trailing expression.


So, how do we make this feature "worth it"? This would make a lot of code generic that would not necessarily have been generic if it were written as a normal fn() -> Result<Ok, Error>.

The first step is to realize that Try is just "isomorphic to Result," at least in the current definition. The compiler could just compile the function as a -> Result<Ok, Error> function, and then insert the required Try conversion boilerplate into the caller if it wanted a different Try type.

But more importantly, I think the value of a try fn goes hand-in-hand with an "error side-channel" / "lightweight exception" mechanism. try fn would be the way you explicitly suggest the compiler use this mechanism.

I don't want to go into exactly what an alternate ABI for Result/Try would look like for Rust; I don't perfectly understand it myself. What I do understand, though, is that it's an optimization of the happy path over the cold path. It's purely an optimization over -> Result semantics, and should be viewed as such: something the compiler can turn on and off depending on whether it thinks the optimization is beneficial (to the non-error path).

But I do think this kind of try fn is useful to explicitly say "hey, the Try::Error case here is cold," and "optimize for the Try::Ok case." But at the same time, it's mostly useful when most calls into try fn are immediately just annotated with ? themselves, passing along the error.

And I think that may be where the disconnect on the usefulness or "viability" of try fn is. For the majority case, I think library code should not be using try fn. try fn should be used for functions that are primarily just "application style," passing up some mostly opaque eyre::ErrReport that will be caught and logged at some top level. For applications, the error case is an expected failure, but one that you can't really do much about other than acknowledge and move on.


As a final step: say we decide try fn is worth it on the argument above, and send it off to the bikeshed factory to await (pun intended) its final syntax form. How do we make it actually useful, rather than just theoretically desirable?

The main one will be defaulting the return type to be Result if the concrete return type is needed (i.e. it's not immediately ? applied). Doing so might allow the standard library to use try fn for things like fs::read_to_string above.

But I also think that it ties in to the greater error handling story in Rust, which is far from a solved problem. The utility of a try fn mechanism is inherently tied to the shape of errors it carries.

Discuss this on Reddit

Posted on by:

cad97 profile

Christopher Durham

@cad97

Rust fan and Programming Language Enthusiast

Discussion

markdown guide