A slightly more general, easier to implement alternative to the Try trait

Adding a from_unwrapped method to the trait basically requires that an object implementing this trait must be reconstructible from its output type. In my Bail example, it broke down because a Bail was always a T, even when the T would never be actually produced.

In the wild, this could break down if people want to implement the TryUnwrap trait on objects that are "more than a bijection from Result". Think Result with state, for instance:

struct RemotePromise<T> {
    connection: Connection,
    request: Request,
    _phantom: PhantomData<T>,
}

impl<T> TryUnwrap<Result<(), ConnectionError>> for RemotePromise<T> {
    type Output = T;
    fn try_unwrap(self) {
        match self.connection.resolve_promise(self.request) {
            Ok(result) => MaybeUnwrap::Unwrap(result),
            Err(error) => MaybeUnwrap::Return(Err(error)),
        }
    }
    fn from_unwrap(result: T) -> Self {
        // cannot rebuild connection nor request from here.
    }
}

Now, this example is still a bit contrived, because we could have a PromiseResult inert object (like Future has Poll) and have that implement the TryUnwrap type, so I'm not sure it is a good example. Besides, the perspective of being able to use ? to perform network operations is a bit frightening.

I'm not sure it's worth it to lose the convenience of from_unwrapped for such a niche case.

Need to think a little bit more about all of this :thinking:

If anyone comes up with a better example, feel free to share!

2 Likes

Ahh, here is the bit I was missing. I now see the value of splitting this in two. "Result + state" seems like something that would come up (even if I can't think of an example at the moment.)

I don't think your example is any good because it seems to do more than just unwrapping a value (it's resolving a connnection, which should return a type that implements TryUnwrap), but I do see what you're getting at. I still don't know of a use-case for a type that is a "Result + state", but I don't think me not being imaginative enough should block this.

Is this something the community wants, though?

Your code reminds me a lot of C++ operator overloading, where developers would write code that's both a lot more concise and infinitely less clear to outside users who aren't familiar the codebase's homebrew overloaded operators.

Personally, if we were to add an "early exit on success" operator to Rust, I'd want that operator to have a different syntax than the try operator, to signal "returning early is what we often want" (as opposed to "an error"). eg:

fn loop_until_found() -> S {
    loop {
         return if find_item();
    }
}

(though the above example conflicts with existing syntax, but you get my point)

1 Like

I wonder if parametrized TryUnwrap could allow using ? on Option in functions returning (). That would satisfy a very common case that currently needs 5 lines of code (if let and return).

1 Like

Sure!

impl<T> TryUnwrap<()> for Option<T> {
    type Output = T;

    fn try_unwrap(self) -> MaybeUnwrap<T, ()> {
        match self {
            Some(u) => MaybeUnwrap::Unwrap(u),
            None => MaybeUnwrap::Return(()),
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_option() {
        fn option_to_none() {
            let _x = q!(Some(42));
            let _y = q!(None);
            unreachable!();
        }
        assert_eq!(option_to_none(), ());
    }
}

panics if I remove the let _y = q!(None); line.

Why not just if (find_item()) { return; }, though?

1 Like

I'm worried about performance, both runtime and compile-time.

To start with, there are already cases where using the ? operator (still) results in worse runtime performance than the old try!, even with optimizations at max. Normally LLVM's optimizer will optimize both into IR which is equally good, if not outright identical. But it takes more steps to do so in ?'s case, thanks to the more-complex desugaring. Apparently, that can cause it to miss potential followon optimizations.

I'm not sure if this has been measured, but the switch to ? probably increased compile times as well; after all, taking more steps should take longer. That said, for all I know the difference might be insignificant.

Also, debug builds of course don't get the optimizations, so they bear the full brunt of increased complexity at runtime. We traditionally don't care too much about debug performance, but it's an additional factor.

I'm mentioning this because the proposals here seem even more complex than the existing Try-based desugaring. For example, this from questionmark's try_unwrap branch:

// impl<T, E, F> TryUnwrap<F> for Result<T, E> where F: FromError<E>
    fn try_unwrap(self) -> MaybeUnwrap<T, F> {
        match self {
            Ok(ok) => MaybeUnwrap::Unwrap(ok),
            Err(err) => MaybeUnwrap::Return(FromError::from_error(err)),
        }
    }

would roughly serve the role of this from the existing standard library:

// impl<T, E> ops::Try for Result<T, E>
    fn into_result(self) -> Self {
        self
    }

...although the former does have the from_error built in, whereas the latter relies on the compiler inserting From::from.

Still, the MaybeUnwrap version adds an extra match, and it also makes the handling of the intermediate object (the MaybeUnwrap in this case) more complicated.

I suppose it's too early at this point to get too deep into performance. Here too, for all I know, the cost might not be significant, or it might even end up being cheaper than Try somehow. We won't know until it's actually implemented. Also, it might be possible to optimize the implementation, e.g. to somehow special-case using ? on known types to bypass the trait (although type inference might make that difficult).

Still... I'd like people to keep performance in mind. And if it does turn out there's a major performance regression, I'd like that to be a sort of 'no-go condition': the new feature shouldn't be stabilized until the regression is fixed, and if it can't be fixed, we should go back to the drawing board.

6 Likes

? is equivalent to a try! macro that expands to the same code, if not slightly more efficient. It is, however, easier on compile times to avoid the From::from call, as that's an extra type inference variable that has to be solved for, for every use of ?. (I do not have an intuition for if moving that into the types of the ? desugaring would help or hurt this.)

It's not concrete, but I know I saw a try! macro equivalent in some large library (serde?) at some point, but which lacked the From::from call. The documentation said it was because the large number of (generated?) uses of the From::from were causing measurable compilation slowdowns, though all of the uses were indeed only using the identity transformation.

Thank you @comex, bringing the performance question on the table is an interesting perspective!

I wonder how we could test the impact on performance of TryUnwrap on some real case scenarios?

Unrelated, but reading Yoshua Wuyts' "nine patches" article, they say:

Because of reasons Try isn't implemented directly on Poll , so simply doing ? on it doesn't work to return early.

Does anyone know of the reasons why Poll does not implement Try? It looks easy to implement TryUnwrap at least:

impl<T, U> TryUnwrap<Poll<U>> for Poll<T> {
    type Unwrap = T;
    fn try_unwrap(self) -> MaybeUnwrap<T, Poll<U>> {
        match self {
            Poll::Ready(t) => MaybeUnwrap::Unwrap(t),
            Poll::Pending => MaybeUnwrap::Return(Poll::Pending),
        }
    }
}

Is it because it might conflict with using ? to return early for Poll<Result<T, E>> when the future is ready and returned a Err(E)?

I wonder if we could have both behaviors at the same time, complete with errors when it is ambiguous, using the TryUnwrap trait?

Exactly this

See also:

We're making some progress here, but it's also blocked on enabling MIR inlining by default as an optimization.

7 Likes

Nice! I'll look forward to that.

1 Like

Found it again! serde_json, here:

// We only use our own error type; no need for From conversions provided by the
// standard library's try! macro. This reduces lines of LLVM IR by 4%.
macro_rules! tri {
    ($e:expr) => {
        match $e {
            crate::lib::Result::Ok(val) => val,
            crate::lib::Result::Err(err) => return crate::lib::Result::Err(err),
        }
    };
}

serde_derive also emits its own copy of try! with the error conversion removed:

// None of our generated code requires the `From::from` error conversion
// performed by the standard library's `try!` macro. With this simplified macro
// we see a significant improvement in type checking and borrow checking time of
// the generated code and a slight improvement in binary size.
pub fn replacement() -> TokenStream {
    // Cannot pass `$expr` to `quote!` prior to Rust 1.17.0 so interpolate it.
    let dollar = Punct::new('$', Spacing::Alone);


    quote! {
        #[allow(unused_macros)]
        macro_rules! try {
            (#dollar __expr:expr) => {
                match #dollar __expr {
                    _serde::export::Ok(__val) => __val,
                    _serde::export::Err(__err) => {
                        return _serde::export::Err(__err);
                    }
                }
            }
        }
    }
}

So while this isn't a performance hit by using ? over try!, it is definitely a hit of using ? over using a match that doesn't do error type conversion.

We'd have to ask @dtolnay to re-measure to see if this has improved since these were first added to serde's codebase, of course.

1 Like

To those in the thread with goals they'd like a new Try design to accomodate, please add them in https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/Try-trait.20wishlist.2C.20pre-meeting ahead of the upcoming design meeting.

I'll ask that, for now, you keep any particular implementation strategies (trait definitions, etc) out of that thread, however.

Yeah, this is the critical point. Generic code needs a way to put something back into the generic type after it got it out by using ?.

3 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.

Followup: @scottmcm just posted RFC 3058, with a proposed new approach for ? and Try.

6 Likes