Enabling types that can be `?` argument but not target

A question came up on URLO about turning a tuple of Results into a Result of tuples, which led me to write a proof-of-concept of a Try implementation for tuples where each element itself implements Try which forwards the first residual encountered.

It mostly works, but I had to write a panicing FromResidual implementation because the residual doesn't contain enough information to reconstruct a tuple that will break down in the same way— If it was the second tuple element that failed, for example, it would need to produce some kind of "successful" result for the first element.

This is only a problem if you attempt to use a tuple as the target of a try operation, which leads me to think that it might be a good idea to change the v2 Try trait a little bit, to something like:

trait Try: IntoResidual + FromResidual<<Self as IntoResidual>::Residual> {}
impl<T> Try for T where T:IntoResidual + FromResidual<<T as IntoResidual>::Residual> {}

trait IntoResidual {
    // current body of Try
}

The argument to the ? could then be any IntoResidual type that the destination has a corresponding FromResidual implementation for, which would allow libraries to produce types that are compatible with ?-interconversion to Result or Option but are not necessarily suitable to receive ? early returns themselves.

2 Likes

This sort of "directionality" is one of the open things on the tracking issue. There's also people wishing for things like (): FromResidual<!>, which wants the other direction where () isn't ?-able but can be created from a residual.

(And it affects coherence too, so it's all messy.)

5 Likes

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.

1 Like