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.
Top comments (0)