"try fn" without special-casing Result
Tags: rust, languagedesign
Published: 2020-04-09 03:01:07

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):

```rust
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

```rust
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](https://github.com/rust-lang/rust/issues/70941).

-----

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"](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0709r0.pdf) 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](https://www.reddit.com/r/rust/comments/fxkpjd/try_fn_without_specialcasing_result)