Pre-RFC: "Fat Try" Another attempt to re-design the Try trait
This is an attempt to re-design the `Try` and `Residual` traits to be simpler while generally maintaining functionality. I call it "fat try" because it stuffs most everything into `Try` instead of breaking it up into several traits.
I believe this is workable solution but the "ControlFlow Wrinkle" below is one non-trivial obstacle. I have a local branch as a POC where I have been able to prove that this generally works, including changes to try desugaring.
* * *
Here are the traits. There's only two:
pub const trait Try {
type Kind;
type Continue;
type Break;
fn from_continue(output: Self::Continue) -> Self;
fn from_break(output: Self::Break) -> Self;
fn branch(self) -> ControlFlow<Self::Break, Self::Continue>;
}
pub const trait TryAs<T>: Try {
type Try: Try<
Kind = Self::Kind,
Break = Self::Break,
Continue = T,
>;
}
Example implementation:
impl<T, E> ops::Try for Result<T, E> {
type Kind = Result<!, !>;
type Continue = T;
type Break = E;
fn from_continue(t: T) -> Self {
Ok(t)
}
fn from_break(e: E) -> Self {
Err(e)
}
fn branch(self) -> ControlFlow<E, T> {
match self {
Err(e) => ControlFlow::Break(e),
Ok(v) => ControlFlow::Continue(v),
}
}
}
impl<T, U, E> TryAs<U> for Result<T, E> {
type Try = Result<U, E>;
}
For `Option`, `Kind` is `Option<!>` and `Break` is `()`.
## Try::Kind
(This is the main thing that makes this proposal different.)
**The`Try::Kind` Rule:** All `?` operators must have a target whose `Try::Kind` is equal to the `Try::Kind` of the result of the surrounding try scope (function or try block).
`Try::Kind` is a marker type (never constructed at runtime) which ascribes a _semantic_ to the `?` operator for the implemented type. The `Try::Kind` for `Result` and `Option` are `Result<!, !>` and `Option<!>` respectively. You can think of it as an answer to the question: "What does the `?` operator _test_ for this type?". `Try::Kind=Result<!, !>` means "`?` tests for the presence of an error". And `Try::Kind=Option<!>` means "`?` tests for the absence of a value". Note that the `Try::Kind` does _not_ necessarily indicate the actual type. For example, you may have `CustomError as Try<Kind=Result<!, !>>`.
## Custom Try Types
When implementing a custom `Try` type, you will need to decide what the `Kind` will be, and whether to re-use a std `Try::Kind` (`Result<!, !>` or `Option<!>`) or define your own. Perhaps the most common choice would be to use `Result<!, !>`, thus declaring your type to be a "custom result", and having the same semantic of "`?` tests the presence of an error". Or you may define a new `Try::Kind` to signify some domain-specific semantic. But then your type cannot be used in try scopes which produce a std type. Choosing a std `Try::Kind` gives you interoperability with the corresponding std type "for free". That is, you may use `?` on your custom error in a try scope that returns `Result` and visa versa.
## `?` uses `From`
The `?` operator includes a `From` conversion to convert the `?` target's `Break` type into the try scope output's `Try::Break` type.
So we can say that these are the full requirements for using `?`:
* The `Try::Kind` rule above
* The `Try::Break` of the `?` target must convert to the `Try::Break` of the try scope result using `From`.
`From` conversion on the `Break` value is **not** included in homogeneous try blocks. More on that later.
## The `TryAs` Trait
The `TryAs` trait draws a relationship between `Try` types that are the same type but with the `Continue` type interchanged. For example, `io::Result<u32> as TryAs<String>::Try = io::Result<String>`. Note that a `TryAs` conversion maintains that `Try::Kind` and `Try::Break` are unchanged. Also note that `TryAs` should maintain the same outer type, so it is more constrained than `Try::Kind`.
The `TryAs` trait is useful in function signatures that are generic over `Try`, and where the `Continue` type is changed. For example, here is a function signature for `std::array::try_from_fn` (some details omitted):
pub fn try_from_fn<T, R, const N: usize, F>(f: F) -> <R as TryAs<[T; N]>>::Try
where
F: FnMut(usize) -> R,
R: Try<Continue = T> + TryAs<[T; N]>
## Homogeneous Try Blocks
Homogeneous try blocks generally work as described in the RFC. The underlying type requirements just look different. Where heterogeneous try scopes use `Try::Kind` to enforce a relationship between types, homogeneous try blocks use `TryAs` instead.
**The Homogeneous`TryAs` Rule:** For every `?` operator within a homogeneous try block, given the `?` target is `T` and the try scope result is `R`, `<T as TryAs<<R as Try>::Continue>> = R`. In other words, the `?` target type can be related to the try scope result type via `TryAs`.
## `ControlFlow` Wrinkle
`ControlFlow` is stabilized with no `From` conversion in its `?` support. I don't know how to resolve this unfortunately. My preference is to use a time machine to reverse the stabilization, but I am unable to supply a time machine at this time.
## Comparison with `try_trait_v2`, discussion
* No "residual" types. The `Break` type in "fat try" is intended to be fully minimized and not carry any information about the originating `Try` type. The burden of defining which `Try` types are interoperable is fully shifted to the `Try` trait itself, particularly with `Try::Kind`. Also there is no more need "unwrap" the residual type in `from_residual`.
* No `FromResidual` implementations. With `Try::Kind`, all `Try` types of the same `Kind` are allowed to be used with each other (a `?` target paired with a try scope result) without implementing a trait for every pair within the set. You only need to implement `From` to convert `Break` types as needed.
* No `Residual` trait. (Generally replaced with `TryAs`)
* Less fine control over which types are inter-convertible. I think this is totally fine. But this is putting some faith in `Try::Kind` that it will make sense across use cases.
* `From` conversion is built into `?`. With `try_trait_v2`, you need to include `From` conversion into `FromResidual` implementations if desired. I think the advent of homogeneous try blocks at least partially negates some of the motive to remove universal `From` support. And I fear that having both variable `From` _and_ two types of try blocks is too much complexity overall.
* `Try::Kind` is kind of strange as a "marker type". Though I believe this is just relocating the strangeness of "residual types" like `Result<!, E>` and giving it a more dedicated place.
* Addition of `TryAs`. This trait might feel a bit ad-hoc, admittedly. You don't need it until you try to use homogeneous blocks or certain functions that use it in the interface.
* `TryAs` implementations tend to have a lot of generic parameters and this can be confusing. I wonder if there is a way to auto-derive these based on the `Try` impl.
* `TryAs` for types with a non-generic `Continue` is possible but strange to implement.
* `TryAs` isn't strict enough - it should be required to be round-trippable: `<R as TryAs<R::Continue>>::Try = R`. This could be a lint.