This approach is a bit extreme, but, I think, worth exploring.
Relevant topics:
UPD: If you don’t want to read the whole discussion I recommend to check this @Centril’s comment.
Summary
Introduce try fn and throw/fail/etc. keyword (throw will be used here for now), which will allow us to write the following code:
try fn foo() -> Result<u64, MyError> {
// returns Ok(10), i.e. happy path is Ok autowrapped
if early_return() { return 10; }
// returns `Err(MyError)`
if is_error() { throw MyError; }
// `?` works as well
check_another_error()?;
1
}
try fn bar() -> Result<(), MyError> {
if is_error() { throw MyError; }
// Good buy `Ok(())`!
}
Additionally try fn will work with Option and even custom types which will implement TryOk and TryThrow.
How it will work
Instead of Try trait we will have the following two traits:
trait TryOk<T> {
fn from_ok(val: T) -> Self;
}
trait TryThrow<T> {
fn from_throw(val: T) -> Self;
}
And Result will have the following implementations:
impl<T, E> TryOk<T> for Result<T, E> {
fn from_ok(val: T) -> Self { Ok(val) }
}
impl<T, E, V: Into<E>> TryThrow<V> for Result<T, E> {
fn from_throw(val: V) -> Self { Err(val.into()) }
}
Now this code:
try fn foo() -> Result<u64, MyError> {
if is_error() { throw MyError; }
1
}
Will be desugared into:
fn foo() -> Result<u64, MyError> {
if is_error() { return TryThrow::from_throw(MyError); }
TryOk::from_ok(1)
}
For Option trait implementations will look like:
impl<T> TryOk<T> for Option<T> {
fn from_ok(val: T) -> Self { Some(val) }
}
impl<T> TryThrow<()> for Option<T> {
fn from_throw(_val: ()) -> Self { None }
}
Why generic type in traits instead of the associated one?
For Result and Option generic type in the Try traits is clearly redundant, but with custom types it can lead to boilerplate reduction and ergonomic improvements. For example this can be in a crate:
enum Record {
Int(i64),
Str(String),
Error(MyErr),
None,
}
impl TryOk<i64> for Record {
fn from_ok(val: i64) -> Self { Record::Int(val) }
}
impl TryOk<String> for Record {
fn from_ok(val: String) -> Self { Record::Str(val) }
}
impl TryOk<i64> for Record {
fn from_ok(val: i64) -> Self { Record::Int(val) }
}
impl<E, T: Into<E>> TryThrow<E> for Record {
fn from_throw(val: T) -> Self { Record::Err(val.into()) }
}
impl TryThrow<()> for Record {
fn from_throw(_val: ()) -> Self { Record::None }
}
And this in user code:
try fn foo(data: &[u8]) -> Record {
if data.len() == 0 { throw; } // returns `Record::None`
if let Err(e1) = check(data) { throw e1; } // e1 converted to MyErr
if let Some(s) = parse_str(data) { return s; } // returns `Record::Str(s)`
if let Some(n) = parse_int(data) { return n; } // returns `Record::Int(n)`
throw "failure message";
}
There is (initially unintended) side-effect: type can implement only TryOk trait, e.g. like this:
impl<T: Into<Foo>> TryOk<T> for Foo {
fn from_ok(val: T) -> Self { val.into() }
}
// no `TryThrow` implementations for `Foo`
Which will make try fns returning Foo “auto-into” functions:
// converts `1` and `"ok"` into `Foo` automatically
try fn foo1() -> Foo {
if true { return 1; }
return "ok";
}
try fn foo2() -> Foo {
throw; // compilation error: `Foo` does not implement `TryThrow` trait
}
Drawbacks
- Overly-flexibile for the main use-cases (
ResultandOption). - Can lead to surprising conversions behind the scenes.
- “Auto-into” variant is a bit surprising usage of
try fn.
Unresolved questions
Bikeshed of trait and keyword names
This feature is closely related to the throw RFC and has the same choices for keywords. Subjectively throw MyError; looks more natural than fail MyError;, thus I think it’s a slightly better option, even considering unwanted relation to exceptions.
Special keyword for “happy” return
It was proposed to add pass keyword to the language, instead of doing autowrapping returns. The big disadvantages are:
- Additional keyword
- Return of
Ok(())
More magic in trait implementations
There is an open question of how far we want to go with traits implementations for Result and Option, e.g. the following implementation can be added:
impl<T, E, V: Into<T>> TryOk<V> for Result<T, E> {
fn from_ok(val: V) -> Self { Ok(val.into()) }
}
Personally I think such implementations are unnecessary, and can result in too much confusion, compared to potential ergonomic improvements.
Relation to try block
Try blocks are closely related to try fns, thus ideally we would like to be able to think about try fn foo() -> T { .. } as a short-cut for fn foo() -> T { try { .. } }.
)