Once you completely split Try
into separate ?
and try{}
, there's then a question of whether we want a "combined" trait at all. And, playing with this a little again, maybe we do; "and_then
" functionality like used by e.g. Iterator::try_find
is a natural thing to find its home in this combination of "throw" and "catch." Perhaps the signatures could even become less inscrutable…
My attempt at working through this:
/// The branch operator `?`.
pub trait Branch {
/// The resulting type after applying the `?` operator.
type Output;
/// The type propagated when the `?` operator short-circuits.
///
/// # Choosing a `Branch::Break` type
///
/// The `?` operator extracts the `T` value from the `Ok` variant of
/// `Result<T, E>`, so it can seem reasonable to use `E` as `Break`.
/// That `do yeet /* value: E */` works in a `try as Result<_, E>`
/// might even suggest that this is true. However, this choice can't
/// generalize; what should an enum with more than two variants use?
/// Additionally, the `Break` type controls what types can be used
/// by `?` in different `try` contexts, and it might not be desirable
/// for
type Break;
fn branch(self) -> ControlFlow<Self::Break, Self::Output>;
}
/// The continue (non short-circuiting) control flow of `try` blocks.
pub trait FromContinue<C> {
fn from_continue(value: C) -> Self;
}
/// The break (short-circuiting) control flow of `try` blocks.
pub trait FromBreak<B> {
/// Convert a value propagated by a `?` short-circuit into `Self`.
fn from_break(value: B) -> Self;
}
/// Extended alias for combined `try` block functionality.
///
/// # Implementing `Try`
///
/// To support `try`'s high generality, `Try` is split into three supertraits.
/// If a type implements all of them, it will automatically implement `Try`.
///
/// For a type which should be interchangable in `try` and `?`, the simplest
/// implementation strategy is directly mirroring `Result`'s implementation:
///
/// ```rust
/// # enum MyResult<T, E> { Ok(T), Err(E) }
/// impl<T, E> Branch for MyResult<T, E> {
/// type Output = T;
/// type Break = Result<!, E>;
/// fn branch(self) -> ControlFlow<Result<!, E>> {
/// # unimplemented!()
/// /* ... */
/// }
/// }
///
/// impl<T, E> FromContinue<T> for MyResult<T, E> {
/// fn from_continue(value: T) -> Self {
/// # unimplemented!()
/// /* ... */
/// }
/// }
///
/// impl<T, E> FromBreak<Result<!, E>> for MyResult<T, E> {
/// fn from_break(Err(value): Result<!, E>) -> Self {
/// # unimplemented!()
/// /* ... */
/// }
/// }
/// ```
///
/// To more finely control what `try` contexts `MyResult` works in, use a
/// different `Branch::Break` type and define interconversions explicitly:
///
/// ```rust
/// # enum MyResult<T, E> { Ok(T), Err(E) }
/// impl<T, E> Branch for MyResult<T, E> {
/// type Output = T;
/// type Break = MyResult<!, E>;
/// fn branch(self) -> ControlFlow<MyResult<!, E>> {
/// # unimplemented!()
/// /* ... */
/// }
/// }
///
/// // try as MyResult { T }
/// impl<T, E> FromContinue<T> for MyResult<T, E> {
/// fn from_continue(value: T) -> Self {
/// # unimplemented!()
/// /* ... */
/// }
/// }
///
/// // try as MyResult { MyResult? }
/// impl<T, E> FromBreak<MyResult<!, E>> for MyResult<T, E> {
/// fn from_break(value: MyResult<!, E>) -> Self {
/// # unimplemented!()
/// /* ... */
/// }
/// }
///
/// // try as MyResult { Result? }
/// impl<T, E> FromBreak<Result<!, E>> for MyResult<T, E> {
/// fn from_break(value: Result<!, E>) -> Self {
/// # unimplemented!()
/// /* ... */
/// }
/// }
///
/// // try as Result { MyResult? }
/// impl<T, E> FromBreak<MyResult<!, E>> for Result<T, E> {
/// fn from_break(value: MyResult<!, E>) -> Self {
/// # unimplemented!()
/// /* ... */
/// }
/// }
/// ```
///
/// For more details, see the relevant trait documentation.
pub trait Try: Branch + FromContinue<Self::Output> + FromBreak<Self::Break> {
/// `Self` but with the "continue" type (`Break::Output`) replaced.
/// Only useful when writing code generic over the used `Try` type.
/*final*/ // can't be overridden by impl; generic usage can be normalized
type Return<R>: Try<Output = R, Break = Self::Break, Return<Self::Output> = Self>
+ TryWith<Self::Output>
= <Self as TryWith<R>>::Type
where
Self: TryWith<R>;
}
impl<T: ?Sized> Try for T where T: Branch + FromContinue<Self::Output> + FromBreak<Self::Break> {}
/// Marker trait for the supported continue types in `Try::Return`.
///
/// An implementation will usually just substitute the relevant type
/// parameter. This trait is used to bound the `Try::Return` GAT such
/// that additional bounds can be added to `C` when they're necessary.
///
/// Reflexivity and transitivity of the type transformation is enforced
/// by the type system, but may not be transparently unified in all cases
/// due to implementation limitations in type resolution.
pub trait TryWith<C>: Try {
type Type: TryWith<C, Output = C, Break = Self::Break>
+ TryWith<Self::Output, Return<Self::Output> = Self>;
}
macro_rules! try_block {
($b:block) => {
'try_block: {
let __helper = __TryHelper(PhantomData);
macro_rules! try_op {
($e:expr) => {
match __helper.branch($e) {
Continue(v) => v,
Break(v) => break 'try_block __helper.from_break(v),
}
};
}
__helper.from_continue($b)
}
};
(as $R:ty; $b:block) => {
'try_block: {
macro_rules! try_op {
($e:expr) => {
match <$R as Branch>::branch($e) {
Continue(v) => v,
Break(v) => break 'try_block <$R as FromBreak<_>>::from_break(v),
}
};
}
<$R as FromContinue<_>>::from_continue($b)
}
};
}
#[doc(hidden)]
pub struct __TryHelper<R>(PhantomData<fn(R) -> R>);
impl<R: Try> __TryHelper<R> {
#[inline]
pub fn branch<Q: Try>(self, r: Q) -> ControlFlow<Q::Break, Q::Output>
where
// this guides the type inference
Q: TryWith<R::Output, Type = R>,
{
r.branch()
}
#[inline]
pub fn from_continue<T>(self, c: T) -> R::Return<T>
where
R: TryWith<T>,
{
FromContinue::from_continue(c)
}
#[inline]
pub fn from_break(self, b: R::Break) -> R {
R::from_break(b)
}
}
impl<R> Copy for __TryHelper<R> {}
impl<R> Clone for __TryHelper<R> {
fn clone(&self) -> Self {
*self
}
}
It's annoying to implement, but I think even these limited docs manage to explain it well enough, and using it isn't that bad either:
fn try_find<Iter, R>(
iter: &mut Iter,
mut f: impl FnMut(&Iter::Item) -> R,
) -> R::Return<Option<Iter::Item>>
where
Iter: Iterator + Sized,
R: Try<Output = bool> + TryWith<Option<Iter::Item>>,
{
try as _ {
'ret: {
for item in iter {
if f(&item)? {
break 'ret Some(item);
}
}
None
}
}
}
fn try_reduce<Iter, R>(
iter: &mut Iter,
mut f: impl FnMut(Iter::Item, Iter::Item) -> R,
) -> R::Return<Option<Iter::Item>>
where
Iter: Iterator + Sized,
R: Try<Output = Iter::Item> + TryWith<Option<Iter::Item>>,
{
try as _ {
'ret: {
let Some(mut acc) = iter.next() else {
break 'ret None;
};
while let Some(item) = iter.next() {
acc = f(acc, item)?;
}
Some(acc)
}
}
}
fn try_collect<Iter, B>(iter: &mut Iter) -> <Iter::Item as Try>::Return<B>
where
Iter: Iterator + Sized,
Iter::Item: Try + TryWith<B>,
B: FromIterator<<Iter::Item as Branch>::Output>,
{
let mut err = None;
let coll = iter::from_fn(|| match iter.next()?.branch() {
Continue(v) => Some(v),
Break(v) => {
err = Some(v);
None
}
})
.collect();
if let Some(err) = err {
FromBreak::from_break(err)
} else {
FromContinue::from_continue(coll)
}
}
and it can do the desired try combinator things without any real issues as well. (The playground link has a bit more working out.)
Where is coherence an issue? I thought since re-rebalancing coherence that just the From
direction was sufficient for any and all desirable impls that can ever be coherent. Did you mean the (type) inference effects?
To that end, the design I sketched through above supports both open-ended and directed inference of the try
block output type without any changes to the inference engine, which I think is cool.