Summary

Replace RFC #1859, try_trait, with a new design

設計(する)
for the currently-unstable Try trait and corresponding
照応する
desugaring for the ? operator.
演算子

The new design

設計(する)
includes support for all intentional interconversions. It proposes removing the accidental interconversions, as a crater run demonstrated that would be feasible, however includes an alternative
代わりのもの、選択肢
system that can support them as a low-support-cost edition mechanism
仕組み、機構
if needed.

This is forward-looking to be compatible with other features, like try {} blocks or yeet e expressions

or Iterator::try_find, but the statuses of those features are not themselves impacted by this RFC.

Motivation

The motivations from the previous

前の
RFC still apply
適用する
(supporting more types, and restricted interconversion). However, new information has come in since the previous
前の
RFC, making people wish for a different approach.

  • Using the "error" terminology is a poor fit for other potential implementations
    実装
    of the trait.
  • The previous
    前の
    RFC's mechanism
    仕組み、機構
    for controlling
    制御する
    interconversions proved ineffective, with inference meaning that people did it unintentionally.
  • It's no longer clear that From should be part of the ? desugaring for all types. It's both more flexible -- making inference difficult -- and more restrictive -- especially without specialization -- than is always desired.
  • An experience report in the tracking issue mentioned that it's annoying to need to make a residual type in common cases.

This RFC proposes a solution that mixes the two major options considered

みなす、考慮する
last time.

  • Like the reductionist approach, this RFC proposes an unparameterized trait with an associated type for the "ok" part, so that the type produced
    産出、産出する
    from the ? operator
    演算子
    on a value is always the same.
  • Like the essentialist approach, this RFC proposes a trait with a generic parameter
    仮引数
    for "error" part, so that different types can be consumed.

Guide-level explanation

The ops::ControlFlow type

This is a simple enum:

#![allow(unused)] fn main() { enum ControlFlow<B, C = ()> { /// Exit the operation without running subsequent phases. Break(B), /// Move on to the next phase of the operation as normal. Continue(C), } }

It's intended for exposing things (like graph traversals or visitor) where you want the user to be able to choose whether to exit early. Using an enum is clearer than just using a bool -- what did false mean again? -- as well as allows

it to carry a value, if desired.

For example, you could use it to expose a simple tree traversal in a way that lets the caller

呼び出し側
exit early if they want:

#![allow(unused)] fn main() { impl<T> TreeNode<T> { fn traverse_inorder<B>(&self, mut f: impl FnMut(&T) -> ControlFlow<B>) -> ControlFlow<B> { if let Some(left) = &self.left { left.traverse_inorder(&mut f)?; } f(&self.value)?; if let Some(right) = &self.right { right.traverse_inorder(&mut f)?; } ControlFlow::Continue(()) } } }

Now, you could write the same thing with Result<(), B> instead. But that would require that the passed-in closure use Err(value) to early-exit the traversal, which can cause

起こす
mental dissonance when that exit is because it successfully found the value for which it was looking. Using ControlFlow::Break(value) instead avoids
避ける、回避する
that prejudice, the same way that break val in a loop doesn't inherently mean success nor failure.

The Try trait

The ops::Try trait describes

記述する
a type's behavior
ふるまい
when used with the ? operator,
演算子
like how the ops::Add trait describes
記述する
its behavior
ふるまい
when used with the + operator.
演算子

At its core, the ? operator

演算子
is about splitting a type into its two parts:

  • The output that will be returned from the ? expression,
    with which the program will continue, and
  • The residual that will be returned to the calling
    呼び出し
    code, as an early exit from the normal flow.

(Oxford's definition

定義
for a residual is "a quantity remaining after other things have been subtracted or allowed
許可する、可能にする
for", thus
それゆえに、従って、
the use here.)

The Try trait also has facilities

施設、設備
for rebuilding a type from either of its parts. This is needed to build the final return value from a function, both in ? and in methods generic over multiple
複数の
types implementing
実装する
Try.

Here's a quick overview of a few standard types which implement

実装する
Try, their corresponding
照応する
output and residual types, and the functions which convert
変換する
between them. (Full details will come later; the goal for now is just to get the general
一般
idea.)

+-------------+ +-------------------+ +-------------------+ | Try::Output | | Try Type | | Try::Residual | +-------------+ Try::branch is Continue +-------------------+ Try::branch is Break +-------------------+ | T | <------------------------ | Result<T, E> | ---------------------> | Result<!, E> | | T | | Option<T> | | Option<!> | | C | ------------------------> | ControlFlow<B, C> | <--------------------- | ControlFlow<B, !> | +-------------+ Try::from_output +-------------------+ Try::from_residual +-------------------+

If you've used ?-on-Result

結果、戻り値
before, that output type is likely unsurprising. Since it's given
与えられた
out directly
直接
from the operator,
演算子
there's not much of a choice.

The residual types, however, are somewhat more interesting. Code using ? doesn't see them directly

直接
-- their usage is hidden inside the desugaring -- so there are more possibilities available. So why are we using these ones specifically?

Most importantly, this gives each family of types (Result

結果、戻り値
s, Options, ControlFlows) their own distinct
区別された/独立した
residual type. That avoids
避ける、回避する
unrestricted interconversion between the different types, the ability to arbitrarily mix them in the same method. For example, like in the traversal example earlier, just because a ControlFlow::Break is also an early exit, that doesn't mean that it should be allowed
許可する、可能にする
to consider
考える、みなす
it a Result::Err -- it might be a success, conceptually. So by giving ControlFlow<X, _> and Result<_, X> different residual types, it becomes a compilation error to use the ? operator
演算子
on a ControlFlow in a method which returns a Result
結果、戻り値
, and vice versa. (There are also ways to allow
許可する、可能にする
interconversion where it's desirable between a particular pair of types.)

🏗️ Note for those familiar with the previous

前の
RFC 🏗️

This is the most critical semantic difference. Structurally this definition

定義
of the trait is very similar
似ている、同様の
to the previous
前の
-- there's still a method splitting the type into a discriminated union between two associated types, and constructors
function Object() { [native code] }
to rebuild it from them. But by keeping the "result-ness" or "option-ness" in the residual type, it gives extra control
制御する
over interconversion that wasn't possible before. The changes other than this are comparatively minor, typically
一般的に、典型的に
either rearrangements to work with that or renamings to change the vocabulary used in the trait.

Using ! is then just a convenient yet efficient

効率のよい
way to create those residual types. It's nice as a user, too, not to need to understand an additional
追加の
type. Just the same "it can't be that one" pattern that's also used in TryFrom, where for example i32::try_from(10_u8) gives a Result<i32, !>, since it's a widening conversion
変換
which cannot fail. Note that there's nothing special going on with ! here -- any uninhabited enum would work fine.

How error conversion
変換
works

One thing The Book mentions, if you recall, is that error values in ? have From::from called

呼び出し
on them, to convert
変換する
from one error type to another.

The previous

前の
section
actually lied to you slightly: there are two traits involved, not just one. The from_residual method is on FromResidual, which is generic so that the implementation
実装
on Result
結果、戻り値
can add that extra conversion.
変換
Specifically,
特に
the trait looks like this:

#![allow(unused)] fn main() { trait FromResidual<Residual = <Self as Try>::Residual> { fn from_residual(r: Residual) -> Self; } }

And while we're showing code, here's the exact definition

定義
of the Try trait:

#![allow(unused)] fn main() { trait Try: FromResidual { type Output; type Residual; fn branch(self) -> ControlFlow<Self::Residual, Self::Output>; fn from_output(o: Self::Output) -> Self; } }

The fact that it's a super-trait like that is why I don't feel bad about the slight lie: Every T: Try always has a from_residual function from T::Residual to T. It's just that some types might offer more.

Here's how Result

結果、戻り値
implements
実装する
FromResidual to do error-conversions:

#![allow(unused)] fn main() { impl<T, E, F: From<E>> FromResidual<Result<!, E>> for Result<T, F> { fn from_residual(x: Result<!, E>) -> Self { match x { Err(e) => Err(From::from(e)), } } } }

But Option doesn't need to do anything exciting, so just has a simple implementation,

実装
taking
とる
advantage of the default parameter:
仮引数

#![allow(unused)] fn main() { impl<T> FromResidual for Option<T> { fn from_residual(x: Self::Residual) -> Self { match x { None => None, } } } }

In your own types, it's up to you to decide how much freedom is appropriate. You can even enable interconversion by defining

定義する
implementations
実装
from the residual types of other families if you'd like. But just supporting your one residual type is ok too.

🏗️ Note for those familiar with the previous

前の
RFC 🏗️

This is another notable difference: The From::from is up to the trait implementation,

実装
not part of the desugaring.

Implementing
実装する
Try for a non-generic type

The examples in the standard library are all generic, so serve

働く、用をなす
as good examples of that, but non-generic implementations
実装
are also possible.

Suppose we're working on migrating some C code to Rust, and it's still using the common "zero is success; non-zero is an error" pattern. Maybe we're using a simple type like this to stay ABI-compatible:

#![allow(unused)] fn main() { #[derive(Debug, Copy, Clone, Eq, PartialEq)] #[repr(transparent)] pub struct ResultCode(pub i32); impl ResultCode { const SUCCESS: Self = ResultCode(0); } }

We can implement

実装する
Try for that type to simplify the code without changing the error model.

First, we'll need a residual type. We can make this a simple newtype, and conveniently there's a type with a niche for exactly

正確に
the value that this can't hold.
保有する、有効である
This is only used inside the desugaring, so we can leave it opaque -- nobody but us will need to create or inspect it.

#![allow(unused)] fn main() { use std::num::NonZeroI32; pub struct ResultCodeResidual(NonZeroI32); }

With that, it's straight-forward to implement

実装する
the traits. NonZeroI32's constructor
function Object() { [native code] }
even does exactly
正確に
the check we need in Try::branch:

#![allow(unused)] fn main() { impl Try for ResultCode { type Output = (); type Residual = ResultCodeResidual; fn branch(self) -> ControlFlow<Self::Residual> { match NonZeroI32::new(self.0) { Some(r) => ControlFlow::Break(ResultCodeResidual(r)), None => ControlFlow::Continue(()), } } fn from_output((): ()) -> Self { ResultCode::SUCCESS } } impl FromResidual for ResultCode { fn from_residual(r: ResultCodeResidual) -> Self { ResultCode(r.0.into()) } } }

Aside: As a nice bonus, the use of a NonZero type in the residual means that <ResultCode as Try>::branch compiles down to a nop on the current nightly. Thanks, enum layout optimizations!

Now, this is all great for keeping the interface that the other unmigrated C code expects, and can even work in no_std if we want. But it might also be nice to give other Rust code that uses it the option to convert

変換する
things into a Result
結果、戻り値
with a more detailed error.

For expository purposes, we'll use this error type:

#![allow(unused)] fn main() { #[derive(Debug, Clone)] pub struct FancyError(String); }

(A real one would probably be more complicated and have a better name, but this will work for what we need here -- it's bigger and needs non-core things to work.)

We can allow

許可する、可能にする
? on a ResultCode in a method returning Result
結果、戻り値
with an implementation
実装
like this:

#![allow(unused)] fn main() { impl<T, E: From<FancyError>> FromResidual<ResultCodeResidual> for Result<T, E> { fn from_residual(r: ResultCodeResidual) -> Self { Err(FancyError(format!("Something fancy about {} at {:?}", r.0, std::time::SystemTime::now())).into()) } } }

The split between different error strategies in this section

is inspired by windows-rs, which has both ErrorCode -- a simple newtype over u32 -- and Error -- a richer type that can capture a stack trace, has an Error trait implementation,
実装
and can carry additional
追加の
debugging information -- where the former can be converted
変換する
into the latter.

Using these traits in generic code

Iterator::try_fold has been stable to call

呼び出し
(but not to implement) for a while now. To illustrate the flow through the traits in this RFC, let's implement
実装する
our own version.

As a reminder, an infallible version of a fold looks something like this:

#![allow(unused)] fn main() { fn simple_fold<A, T>( iter: impl Iterator<Item = T>, mut accum: A, mut f: impl FnMut(A, T) -> A, ) -> A { for x in iter { accum = f(accum, x); } accum } }

So instead of f returning just an A, we'll need it to return some other type that produces

産出、産出する
an A in the "don't short circuit" path. Conveniently, that's also the type we need to return from the function.

Let's add a new generic parameter

仮引数
R for that type, and bound
制限する、結び付けられて
it to the output type that we want:

#![allow(unused)] fn main() { fn simple_try_fold_1<A, T, R: Try<Output = A>>( iter: impl Iterator<Item = T>, mut accum: A, mut f: impl FnMut(A, T) -> R, ) -> R { todo!() } }

Try is also the trait we need to get the updated accumulator from f's return value and return the result

結果、戻り値
if we manage to get through the entire iterator:

#![allow(unused)] fn main() { fn simple_try_fold_2<A, T, R: Try<Output = A>>( iter: impl Iterator<Item = T>, mut accum: A, mut f: impl FnMut(A, T) -> R, ) -> R { for x in iter { let cf = f(accum, x).branch(); match cf { ControlFlow::Continue(a) => accum = a, ControlFlow::Break(_) => todo!(), } } R::from_output(accum) } }

We'll also need FromResidual::from_residual to turn the residual back into the original type. But because it's a supertrait of Try, we don't need to mention it in the bounds.

制限する、結び付けられて
All types which implement
実装する
Try can always be recreated from their corresponding
照応する
residual, so we'll just call
呼び出し
it:

#![allow(unused)] fn main() { pub fn simple_try_fold_3<A, T, R: Try<Output = A>>( iter: impl Iterator<Item = T>, mut accum: A, mut f: impl FnMut(A, T) -> R, ) -> R { for x in iter { let cf = f(accum, x).branch(); match cf { ControlFlow::Continue(a) => accum = a, ControlFlow::Break(r) => return R::from_residual(r), } } R::from_output(accum) } }

But this "call

呼び出し
branch, then match
一致する、マッチさせる
on it, and return if it was a Break" is exactly
正確に
what happens inside the ? operator.
演算子
So rather than do all this manually, we can just use ? instead:

#![allow(unused)] fn main() { fn simple_try_fold<A, T, R: Try<Output = A>>( iter: impl Iterator<Item = T>, mut accum: A, mut f: impl FnMut(A, T) -> R, ) -> R { for x in iter { accum = f(accum, x)?; } R::from_output(accum) } }

Reference-level explanation

ops::ControlFlow

#![allow(unused)] fn main() { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ControlFlow<B, C = ()> { /// Exit the operation without running subsequent phases. Break(B), /// Move on to the next phase of the operation as normal. Continue(C), } }

The traits

#![allow(unused)] fn main() { pub trait Try: FromResidual { /// The type of the value consumed or produced when not short-circuiting. type Output; /// A type that "colours" the short-circuit value so it can stay associated /// with the type constructor from which it came. type Residual; /// Used in `try{}` blocks to wrap the result of the block. fn from_output(x: Self::Output) -> Self; /// Determine whether to short-circuit (by returning `ControlFlow::Break`) /// or continue executing (by returning `ControlFlow::Continue`). fn branch(self) -> ControlFlow<Self::Residual, Self::Output>; } pub trait FromResidual<Residual = <Self as Try>::Residual> { /// Recreate the type implementing `Try` from a related residual fn from_residual(x: Residual) -> Self; } }

Expected laws

What comes out is what you put in:

  • <T as Try>::from_output(x).branch() ControlFlow::Continue(x) (aka try { x }? x)
  • <T as Try>::from_residual(x).branch() ControlFlow::Break(x) (maybe aka something like try { yeet e } Err(e), see the future possibilities)

You can recreate what you split up:

  • match
    一致する、マッチさせる
    x.branch() { ControlFlow::Break(r) => Try::from_residual(r), ControlFlow::Continue(v) => Try::from_output(v) }
    x (aka try { x? } x)

Desugaring ?

The previous

前の
desugaring of x? was

#![allow(unused)] fn main() { match Try::into_result(x) { Ok(v) => v, Err(e) => return Try::from_error(From::from(e)), } }

The new one is very similar:

似ている、同様の

#![allow(unused)] fn main() { match Try::branch(x) { ControlFlow::Continue(v) => v, ControlFlow::Break(r) => return FromResidual::from_residual(r), } }

The critical difference is that conversion

変換
(such as From::from) is left up to the implementation
実装
instead of forcing it in the desugar.

Standard implementations
実装

Result
結果、戻り値

#![allow(unused)] fn main() { impl<T, E> ops::Try for Result<T, E> { type Output = T; type Residual = Result<!, E>; #[inline] fn from_output(c: T) -> Self { Ok(c) } #[inline] fn branch(self) -> ControlFlow<Self::Residual, T> { match self { Ok(c) => ControlFlow::Continue(c), Err(e) => ControlFlow::Break(Err(e)), } } } impl<T, E, F: From<E>> ops::FromResidual<Result<!, E>> for Result<T, F> { fn from_residual(x: Result<!, E>) -> Self { match x { Err(e) => Err(From::from(e)), } } } }

Option

#![allow(unused)] fn main() { impl<T> ops::Try for Option<T> { type Output = T; type Residual = Option<!>; #[inline] fn from_output(c: T) -> Self { Some(c) } #[inline] fn branch(self) -> ControlFlow<Self::Residual, T> { match self { Some(c) => ControlFlow::Continue(c), None => ControlFlow::Break(None), } } } impl<T> ops::FromResidual for Option<T> { fn from_residual(x: <Self as ops::Try>::Residual) -> Self { match x { None => None, } } } }

Poll

These reuse Result

結果、戻り値
's residual type, and thus
それゆえに、従って、
interconversion between Poll and Result
結果、戻り値
is allowed
許可する、可能にする
without needing additional
追加の
FromResidual implementations
実装
on Result
結果、戻り値
.

#![allow(unused)] fn main() { impl<T, E> ops::Try for Poll<Result<T, E>> { type Output = Poll<T>; type Residual = <Result<T, E> as ops::Try>::Residual; fn from_output(c: Self::Output) -> Self { c.map(Ok) } fn branch(self) -> ControlFlow<Self::Residual, Self::Output> { match self { Poll::Ready(Ok(x)) => ControlFlow::Continue(Poll::Ready(x)), Poll::Ready(Err(e)) => ControlFlow::Break(Err(e)), Poll::Pending => ControlFlow::Continue(Poll::Pending), } } } impl<T, E, F: From<E>> ops::FromResidual<Result<!, E>> for Poll<Result<T, F>> { fn from_residual(x: Result<!, E>) -> Self { match x { Err(e) => Poll::Ready(Err(From::from(e))), } } } }
#![allow(unused)] fn main() { impl<T, E> ops::Try for Poll<Option<Result<T, E>>> { type Output = Poll<Option<T>>; type Residual = <Result<T, E> as ops::Try>::Residual; fn from_output(c: Self::Output) -> Self { c.map(|x| x.map(Ok)) } fn branch(self) -> ControlFlow<Self::Residual, Self::Output> { match self { Poll::Ready(Some(Ok(x))) => ControlFlow::Continue(Poll::Ready(Some(x))), Poll::Ready(Some(Err(e))) => ControlFlow::Break(Err(e)), Poll::Ready(None) => ControlFlow::Continue(Poll::Ready(None)), Poll::Pending => ControlFlow::Continue(Poll::Pending), } } } impl<T, E, F: From<E>> ops::FromResidual<Result<!, E>> for Poll<Option<Result<T, F>>> { fn from_residual(x: Result<!, E>) -> Self { match x { Err(e) => Poll::Ready(Some(Err(From::from(e)))), } } } }

ControlFlow

#![allow(unused)] fn main() { impl<B, C> ops::Try for ControlFlow<B, C> { type Output = C; type Residual = ControlFlow<B, !>; fn from_output(c: C) -> Self { ControlFlow::Continue(c) } fn branch(self) -> ControlFlow<Self::Residual, C> { match self { ControlFlow::Continue(c) => ControlFlow::Continue(c), ControlFlow::Break(b) => ControlFlow::Break(ControlFlow::Break(b)), } } } impl<B, C> ops::FromResidual for ControlFlow<B, C> { fn from_residual(x: <Self as ops::Try>::Residual) -> Self { match x { ControlFlow::Break(r) => ControlFlow::Break(r), } } } }

Use in Iterator

The provided

与える
implementation
実装
of try_fold is already just using ? and try{}, so doesn't change. The only difference is the name of the associated type in the bound:
制限する、結び付けられて

#![allow(unused)] fn main() { fn try_fold<B, F, R>(&mut self, init: B, mut f: F) -> R where Self: Sized, F: FnMut(B, Self::Item) -> R, R: Try<Output = B>, { let mut accum = init; while let Some(x) = self.next() { accum = f(accum, x)?; } try { accum } } }

Drawbacks

  • While this handles a known accidental stabilization, it's possible that there's something else unknown that will keep this from being doable while meeting Rust's stringent stability guarantees.
    保証する
  • The extra complexity of this approach, compared
    比較する
    to either of the alternatives
    代わりのもの、選択肢
    considered
    みなす、考慮する
    the last time around, might not be worth it.
  • This is the fourth attempt at a design
    設計(する)
    in this space, so it might not be the right one either.
  • As with all overloadable operators,
    演算子
    users might implement
    実装する
    this to do something weird.
  • In situations where extensive interconversion is desired, this requires more implementations.
    実装
  • Moving From::from from the desugaring to the implementations
    実装
    means that implementations
    実装
    which do want it are more complicated.

Rationale and alternatives
代わりのもの、選択肢

Why ControlFlow pulls its weight

The previous

前の
RFC discussed having such a type, but ended up deciding that defining
定義する
a new type for the desugar wasn't worth it, and just used Result
結果、戻り値
.

This RFC does use a new type because one already exists in nightly under the control_flow_enum feature gate. It's being used in the library and the compiler, demonstrating that it's useful beyond just this desugaring, so the desugar might as well use it too for extra clarity. There are also ecosystem changes waiting on something like it, so it's not just a compiler-internal need.

Methods on ControlFlow

On nightly there are a variety of methods available on ControlFlow. However, none of them are needed for the stabilization of the traits, so they left out of this RFC. They can be considered

みなす、考慮する
by libs at a later point.

There's a basic set

セットする、集合
of simple ones that could be included if desired, though:

#![allow(unused)] fn main() { impl<B, C> ControlFlow<B, C> { fn is_break(&self) -> bool; fn is_continue(&self) -> bool; fn break_value(self) -> Option<B>; fn continue_value(self) -> Option<C>; } }

Traits for ControlFlow

ControlFlow derives a variety of traits where they have obvious behaviour. It does not, however, derive

派生する
PartialOrd/Ord. They're left out as it's unclear which order,
順序
if any, makes sense between the variants.

For Options, None < Some(_), but for Result

結果、戻り値
s, Ok(_) < Err(_). So there's no definition
定義
for ControlFlow that's consistent with the isomorphism to both types.

Leaving it out also leaves us free to change the ordering of the variants in the definition

定義
in case doing so can allow
許可する、可能にする
us to optimize
最適化する
the ? operator.
演算子
(For a similar
似ている、同様の
previous
前の
experiment, see PR #49499.)

Naming the variants on ControlFlow

The variants are given

与えられた
those names as they serve
働く、用をなす
the same purpose as the corresponding
照応する
keywords when used in Iterator::try_fold or Iterator::try_for_each.

For example, this (admittedly contrived) loop

#![allow(unused)] fn main() { let mut sum = 0; for x in iter { if x % 2 == 0 { continue } sum += x; if sum > 100 { break } continue } }

can be written as

#![allow(unused)] fn main() { let mut sum = 0; iter.try_for_each(|x| { if x % 2 == 0 { return ControlFlow::Continue(()) } sum += x; if sum > 100 { return ControlFlow::Break(()) } ControlFlow::Continue(()) }); }

(Of course, one wouldn't normally use the continue keyword at the end of a for loop like that, but I've included it here to emphasize that even the ControlFlow::Continue(()) as the final expression

of the block it ends up working like the keyword would.)

Why ControlFlow has C = ()

The type that eventually became ControlFlow was originally added

たす
way back in 2017 as the internal-only type LoopState used to make some default implementations
実装
in Iterator easier to read. It had no type parameter
仮引数
defaults.

Issue #75744 in 2020 started the process of exposing it, coming out of the observation that Iterator::try_fold isn't a great replacement for the deprecated-at-the-time Itertools::fold_while since using Err for a conceptual success makes code hard to read.

The compiler actually had its own version of the type in librustc_data_structures at the time:

#![allow(unused)] fn main() { pub enum ControlFlow<T> { Break(T), Continue, } }

The compiler was moved over to the newly-exposed type, and that inspired the creation of MCP#374, TypeVisitor: use ops::ControlFlow instead of bool. Experience from that lead to flipping the type arguments

引数
in PR#76614 -- which also helped the original use cases in Iterator, where things like default implementation
実装
of find also want C = (). And these were so successful that it lead to MCP#383, TypeVisitor: do not hard-code a ControlFlow<()> result,
結果、戻り値
having the visitors use ControlFlow<Self::BreakTy>.

As an additional

追加の
anecdote that C = () is particularly common, Hytak mentioned the following
下記の、次に続く、追従する
on Discord in response to seeing a draft of this RFC:

i didn't read your proposal in depth, but this reminds me of a recursive search function i experimented with a few days ago. It used a Result

結果、戻り値
type as output, where Err(value) meant that it found the value and Ok(()) meant that it didn't find the value. That way i could use the ? to exit early

So when thinking about ControlFlow, it's often best to think of it not like Result

結果、戻り値
, but like an Option which short-circuits the other variant. While it can flow a Continue value, that seems to be a fairly uncommon use in practice.

Was this considered
みなす、考慮する
last time?

Interestingly, a previous

version of RFC #1859 did actually mention a two-trait solution, splitting the "associated type for ok" and "generic type for error" like is done here. It's no longer mentioned in the version that was merged. To speculate, it may have been unpopular due to a thought that an extra traits just for the associated type wasn't worth it.

Current desires for the solution, however, have more requirements than were included in the RFC at the time of that version. Notably, the stabilized Iterator::try_fold method depends on being able to create a Try type from the accumulator. Including such a constructor

function Object() { [native code] }
on the trait with the associated type helps that separate trait provide value.

Also, ok-wrapping was decided in #70941, which needs such a constructor,

function Object() { [native code] }
making this "much more appealing".

Why not make the output a generic type?

It's helpful that type information can flow both ways through ?.

  • In the forward direction,
    方向
    not needing a contextual type means that println!("{}", x?) works instead of needing a type annotation. (It's also just less confusing to have ? on the same type always produce
    産出する
    the same type.)
  • In the reverse direction,
    方向
    it allows
    許可する、可能にする
    things like let x: i32 = s.parse()?; to infer the requested type from that annotation, rather than requiring it be specified
    特定する、指定する、規定する
    again.

Similar

似ている、同様の
scenarios exist for try, though of course they're not yet stable:

  • let y: anyhow::Result<_> = try { x }; doesn't need to repeat the type of x.
  • let x: i16 = { 4 }; works for infallible code, so for consistency it's good for let x: anyhow::Result<i16> = try { 4 }; to also work (rather than default the literal to i32 and fail).

Why does FromResidual take
とる
a generic type?

The simplest case is that the already-stable error conversions

変換
require a generic somewhere in the error path in the desugaring. In the RFC #1859 implementation,
実装
that generic comes from using From::from in the desugaring.

However, more experience with trying to use Try for scenarios other than "the early exit is an error" have shown that forcing this on everything is inappropriate. ControlFlow, for example, would rather not have it, for the same kinds of reasons that return and break-from-loop don't implicitly

暗黙的に
call
呼び出し
it. Option may not care, as it only ever gets applied
適用する
for NoneNone, but that's not really a glowing endorsement.

But even for the error path, forcing From causes

起こす
problems, notably because of its identity
同一性
impl. anyhow's Error type, for example, doesn't implement
実装する
std::error::Error because that would prevent
防ぐ
it from being From-convertible from any E: std::error::Error type. The error handling project group under libs has experimented with a prototype toolchain with this RFC implemented,
実装する
and is excited at the possibilities that could come from being free of this restriction:
制限

my mind is exploding, the possibility of all error types implementing

実装する
error the way they actually should has such massive implications for the rest of the error reporting stuff we've been working on

As a bonus, moving conversion

変換
(if any) into the FromResidual implementation
実装
may actually speed up the compiler -- the simpler desugar means generating
生成する
less HIR, and thus
それゆえに、従って、
less work for everything thereafter (up to LLVM optimizations, at least). The serde crate has their own macro for error propagation which omits
省略する
From-conversion as they see a "significant improvement" from doing so.

Why not merge Try and FromResidual?

This RFC treats

取り扱う
them as conceptually the same trait -- there are no types proposed here to implement
実装する
FromResidual<_> which don't also implement
実装する
Try -- so one might wonder why they're not merged into one Try<R>. After all, that would seem to remove the duplication between the associated type and the generic type, as something like

#![allow(unused)] fn main() { trait Try<Residual> { type Output; fn branch(self) -> ControlFlow<Residual, Self::Output>; fn from_residual(r: Residual) -> Self; fn from_output(x: Self::Output) -> Self; } }

This, however, is technically too much freedom. Looking at the error propagation case, it would end up calling

呼び出し
both Try<?R1>::branch and Try<?R2>::from_residual. With the implementation
実装
for Result
結果、戻り値
, where those inference variables
変数、ストレージ
go through From, there's no way to pick what they should be, similar
似ている、同様の
to how .into().into() doesn't compile. And even outside the desugaring, this would make Try::from_output(x) no longer work, since the compiler would (correctly) insist that the desired residual type be specified.
特定する、指定する、規定する

And even for a human, it's not clear that this freedom is helpful. While any trait can be implemented

実装する
weirdly, one good part of RFC #1859 that this one hopes to retain is that one doesn't need to know contextual information to understand what comes out of ?. Whereas any design
設計(する)
that puts branch on a generic trait would mean it'd be possible for ? to return different things depending on that generic type parameter
仮引数
-- unless the associated type were split out into a separate trait, but that just reopens the "why are they different traits" conversation again, without solving the other issues.

This RFC introduces the residual concept as it was helpful to have a name to talk about in the guide section.

(A previous
前の
version proved unclear, perhaps in part due to it being difficult to discuss something without naming it.) But the fn branch(self) -> ControlFlow<Self::Residual, Self::Output> API is not necessarily obvious.

A different scheme might be clearer for people. For example, there's some elegance to matching

一致する、マッチさせる
the variant names by using fn branch(self) -> ControlFlow<Self::Break, Self::Continue>. Or perhaps there are more descriptive names, like KeepGoing/ShortCircuit.

As a sketch, one of those alternatives

代わりのもの、選択肢
might look something like this:

#![allow(unused)] fn main() { trait Try: FromBreak { type Break; type Continue; fn branch(self) -> ControlFlow<Self::Break, Self::Continue>; fn from_continue(c: Self::Continue) -> Self; } trait FromBreak<B = <Self as Try>::Break> { fn from_break(b: B) -> Self; } }

However the "boring" Output name does have the advantage that one doesn't need to remember a special name, as it's the same as the other operator

演算子
traits. (For precedent, it's Add::Output and Div::Output even if one could argue that Add::Sum or Div::Quotient would be more "correct", in a sense.)

Per feedback from T-libs, this is left as an unresolved question for the RFC, to be resolved in nightly.

Splitting up Try more

This RFC encourages one to think of a Try type holistically, as something that supports all three of the core operations,

演算、操作
with expected rules between them.

That's not necessarily the way it should go. It could be different, like there's no guarantee

保証する
that Add and AddAssign work consistently, nor that Add and Sub are inverses.
逆の

Notably, the this proposal has both an introduction

はじめに、導入
rule (Try::from_output) and elimination rule (Try::branch), in the Gentzian sense, on the same trait. That means that an implementor will need to support both, which could restrict
制限する
the set
セットする、集合
of type with which ? (and try and yeet) could be used.

One unknown question here is whether this is important for any FFI scenarios. Often error APIs come in pairs (like Win32's GetLastError and SetLastError), but some libraries may only give them out without allowing

許可する、可能にする
updating them to a custom value. It's unclear whether such a thing would want to be exposed as ? on some ZST, and thus
それゆえに、従って、
would need a trait split to work, or whether it would be sufficient to load such things into a ?-supporting type where supporting from_residual would be simple.

In pure rust, one could also imagine types where it might be interesting to allow

許可する、可能にする
introduction
はじめに、導入
rules but not elimination rules. With try blocks, one could perhaps have something like

#![allow(unused)] fn main() { let _: IgnoreAllErrors = try { foo()?; bar()?; qux()?; }; }

which works by allowing

許可する、可能にする
from_residual from any Result<_, _>::Residual, as well as from_output from (). On such a type there's no real use in allowing
許可する、可能にする
? on the result,
結果、戻り値
but at the same time it wouldn't be a hardship to offer it.

The split currently in the proposal, though it's there for other reasons, would allow

許可する、可能にする
a small version of this: it would be possible to add an implementation
実装
like impl FromResidual<Result<!, !>> for (), which would allow
許可する、可能にする
code like u64::try_from(123_u16)? even in a method that returns unit. That has a number of issues, however, like only supporting -> () and not other things like -> i32 where one would probably also expect it to work, and it could not be a generic implementation
実装
without some form
形式、形態、形作る
of specialization, as it would conflict with the desired implementation
実装
on Result
結果、戻り値
. And even if it did work, it's not clear that allowing
許可する、可能にする
? here is the clearest option -- other options such as an always_ok method on Result<T, !> might be superior anyway.

Another downside of the flexibility is that the structure of the traits would be somewhat more complicated.

The simplest split would just move each method to its own trait,

#![allow(unused)] fn main() { trait Branch { type Output; type Residual; fn branch(self) -> ControlFlow<Self::Residual, Self::Output>; } trait FromOutput { type Output; fn from_output(x: Self::Output) -> Self; } trait FromResidual<R> { fn from_residual(x: R) -> Self; } }

but that loses the desired property

特徴、性質
that the returned-by-? and expected-by-try types match
一致する、マッチさせる
for types which do implement
実装する
both.

One way to fix that would be to add another trait for that associated type, perhaps something like

#![allow(unused)] fn main() { trait TryBase { type Output; } trait Branch: TryBase { type Residual; fn branch(self) -> ControlFlow<Self::Residual, Self::Output>; } trait FromOutput: TryBase { fn from_output(x: Self::Output) -> Self; } trait FromResidual<R> { fn from_residual(x: R) -> Self; } }

But this has still lost the simplicity

単純さ、簡単さ
of the R: Try bound
制限する、結び付けられて
for use in simple cases like try_fold. (And, in fact, all designs
設計(する)
that allow
許可する、可能にする
types to choose them independently have that issue.) That may mean that it would also be useful to add yet another item, a trait alias
別名
to tie everything together in the "usual" way again. Perhaps it would look something like this:

#![allow(unused)] fn main() { trait Try = Branch + FromOutput + FromResidual<<Self as Branch>::Residual>; }

There are probably also useful intermediary designs

設計(する)
here. Perhaps the IgnoreAllErrors example above suggests that introduction
はじめに、導入
on its own is reasonable, but elimination should require that both be supported. That's also the direction
方向
that would make sense for ? in infallible functions: it's absolutely undesirable for ()????? to compile, but it might be fine for all return types to support something like T: FromResidual<!> eventually.

Per feedback from T-libs, this is left as an unresolved question for the RFC, to be resolved in nightly.

Why a "residual" type is better than an "error" type

Most importantly, for any type generic in its "output type" it's easy to produce

産出する
a residual type using an uninhabited type. That works for Option -- no NoneError residual type needed -- as well as for the StrandFail<T> type from the experience report. And thanks to enum layout optimizations, there's no space overhead to doing this: Option<!> is a ZST, and Result<!, E> is no larger than E itself. So most of the time one will not need to define
定義する
anything additional.
追加の

In those cases where a separate type is needed, it's still easier to make a residual type because they're transient and thus

それゆえに、従って、
can be opaque: there's no point at which a user is expected to do anything with a residual type other than convert
変換する
it back into a known Try type. This is different from the previous
前の
design,
設計(する)
where less-restrictive interconversion meant that anything could be exposed via a Result
結果、戻り値
. That has lead to requests, such as for NoneError to implement
実装する
Error
, that are perfectly understandable given
与えられた
that the instances
実例
are exposed in Result
結果、戻り値
s. As residual types aren't ever exposed like that, it would be fine for them to implement
実装する
nothing but FromResidual (and probably Debug), making them cheap to define
定義する
and maintain.

Use of !

This RFC uses ! to be concise. It would work fine with convert::Infallible instead if ! has not yet stabilized, though a few more match

一致する、マッチさせる
arms would be needed in the implementations.
実装
(For example, Option::from_residual would need Some(c) => match
一致する、マッチさせる
c {}
.)

Why FromResidual is the supertrait

It's nicer for try_fold implementations

実装
to just mention the simpler Try name. It being the subtrait means that code needing only the basic scenario can just bound
制限する、結び付けられて
on Try and know that both from_output and from_residual are available.

Default Residual on FromResidual

The default here is provided

与える
to make the basic case simple. It means that when implementing
実装する
the trait, the simple case (like in Option) doesn't need to think about it -- similar
似ている、同様の
to how you can impl Add for Foo for the homogeneous case even though that trait also has a generic parameter.
仮引数

FromResidual::from_residual vs Residual::into_try

Either of these directions

方向
could be made to work. Indeed, an early experiment while drafting this had a method on a required trait for the residual that created the type implementing
実装する
Try (not just the associated type). However that method was removed as unnecessary once from_residual was added,
たす
and then the whole trait was moved to future work in order
順序
to descope the RFC, as it proved unnecessary for the essential ?/try_fold functionality.

A major advantage of the FromResidual::from_residual direction

方向
is that it's more flexible with coherence when it comes to allowing
許可する、可能にする
other things to be converted
変換する
into a new type being defined.
定義する
That does come at the cost of higher restriction
制限
on allowing
許可する、可能にする
the new type to be converted
変換する
into other things, but reusing a residual can also be used for that scenario.

Converting

変換する
a known residual into a generic Try type seems impossible (unless it's uninhabited), but consuming arbitrary
任意の
residuals could work -- imagine something like

#![allow(unused)] fn main() { impl<R: std::fmt::Debug> FromResidual<R> for LogAndIgnoreErrors { fn from_residual(h: H) -> Self { dbg!(h); Self } } }

(Not that that's necessarily a good idea -- it's plausibly too generic. This RFC definitely isn't proposing it for the standard library.)

And, ignoring

無視する
the coherence implications, a major difference between the two sides is that the target type is typically
一般的に、典型的に
typed out visibly (in a return type) whereas the source
元の
type (going into the ?) is often the result
結果、戻り値
of some called
呼び出し
function. So it's preferable for any behaviour extensions to be on the type that can more easily be seen in the code.

Can we just remove the accidental interconversions?

This depends on how we choose to read the rules around breaking changes.

A crater run on a prototype implementation

found that some people are doing this. PRs have been sent to the places that broke, and generally it was agreed that removing the mixing improved the code:

Definitely a good change.

Thanks for spotting that, that was indeed a confusing mix

However another instance

実例
is in an abandoned project where the repository has been archived, so will not be fixed. And of course if it happened 3 times, there might be more instances
実例
in the wild.

The interesting pattern boils down to this:

#![allow(unused)] fn main() { .map(|v| Ok(something_returning_option(v)?)) }

That means it's using ? on an Option, but the closure ends up returning Result<_, NoneError> without needing to name the type as trait resolution discovers that it's the only possibility. It seems reasonable that this could happen accidentally while refactoring. That does mean, however, that the breakage could also be considered

みなす、考慮する
"allowed"
許可する、可能にする
as an inference change, and hypothetically additional
追加の
implementations
実装
could make it ambiguous in the future. (It's like the normal AsRef breakage, and fits the pattern of "there's a way it could be written that works before and after", though in this case the disambiguated form
形式、形態、形作る
requires naming an unstable type.)

This RFC thus

それゆえに、従って、
proposes removing the accidental interconversions.

Compatibility
互換性
with accidental interconversions (if needed)

If something happens that turns out they need to be supported, the following

下記の、次に続く、追従する
approach can work.

This would take

とる
a multi-step approach:

  • Add a new never-stable FromResidualLegacy trait
  • Have a blanket implementation
    実装
    so that users interact only with FromResidual
  • Add implementations
    実装
    for the accidental interconversions
  • Use FromResidualLegacy in the desugaring, perhaps only for old editions

This keeps them from being visible in the trait system on stable, as FromResidual (the only form

形式、形態、形作る
that would ever stabilize, or even be mentionable) would not include them.

#![allow(unused)] fn main() { mod sadness { use super::*; /// This includes all of the [`ops::FromResidual`] conversions, but /// also adds the two interconversions that work in 2015 & 2018. /// It will never be stable. pub trait FromResidualLegacy<R> { fn from_residual_legacy(r: R) -> Self; } impl<T: ops::FromResidual<R>, R> FromResidualLegacy<R> for T { fn from_residual_legacy(r: R) -> Self { <Self as ops::FromResidual<R>>::from_residual(r) } } /// This is a remnant of the old `NoneError` which is never going to be stabilized. /// It's here as a snapshot of an oversight that allowed this to work in the past, /// so we're stuck supporting it even though we'd really rather not. /// This will never be stabilized; use [`Option::ok_or`] to mix `Option` and `Result`. #[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)] pub struct LegacyNoneError; impl<T, E> ops::FromResidual<Option<!>> for Result<T, E> where E: From<LegacyNoneError>, { fn from_residual(x: Option<!>) -> Self { match x { None => Err(From::from(LegacyNoneError)), } } } #[unstable(feature = "try_trait_v2", issue = "42327")] impl<T> FromResidualLegacy<Result<!, LegacyNoneError>> for Option<T> { fn from_residual_legacy(_: Result<!, LegacyNoneError>) -> Self { None } } } }

Prior art

Previous

前の
approaches used on nightly

This is definitely monadic. One can define

定義する
the basic monad operations
演算、操作
for the Maybe monad as

use std::ops::Try; fn monad_unit<T: Try>(x: <T as Try>::Ok) -> T { T::from_output(x) } fn monad_bind<T1: Try<Residual = R>, T2: Try<Residual = R>, R>(mx: T1, f: impl FnOnce(<T1 as Try>::Ok) -> T2) -> T2 { let x = mx?; f(x) } fn main() { let mx: Option<i32> = monad_unit(1); let my = monad_bind(mx, |x| Some(x + 1)); let mz = monad_bind(my, |x| Some(-x)); assert_eq!(mz, Some(-2)); }

However, like boats described

for async.await, using monads directly
直接
isn't a great fit for rust. ? desugaring to a return (rather than closures) mixes better with the other control
制御する
flow constructs,
作る、構成体
such as break and continue, that don't work through closures. And while the definitions
定義
above work fine for Option, they don't allow
許可する、可能にする
the error-conversion that's already stable with Result
結果、戻り値
, so any monad-based implementation
実装
of ? wouldn't be able to be the normal monad structure regardless.
〜に関わらず

Unresolved questions

Questions from T-libs to be resolved in nightly:

  • What vocabulary should Try use in the associated types/traits? Output+residual, continue+break, or something else entirely?
  • Is it ok for the two traits to be tied together closely, as outlined here, or should they be split up further
    さらなる、それ以上
    to allow
    許可する、可能にする
    types that can be only-created or only-destructured?

Implementation
実装
and Stabilization Sequencing

  • ControlFlow is implemented
    実装する
    in nightly already.
  • The traits and desugaring could go into nightly immediately.
    直後に、直接的に
  • That would allow
    許可する、可能にする
    ControlFlow to be considered
    みなす、考慮する
    for stabilizating, as the new desugaring would keep from stabilizing any unwanted interconversions.
  • Beta testing might result
    結果、戻り値
    in reports requiring that the accidental interconversions be added
    たす
    back in old editions, due to crater-invisible code.
  • Then the unresolved naming & structure questions need to be addressed before Try could stabilize.

Future possibilities

While it isn't directly

直接
used in this RFC, a particular residual type can be used to define
定義する
a "family" of types which all share that residual.

For example, one could define

定義する
a trait like this one:

#![allow(unused)] fn main() { pub trait GetCorrespondingTryType<TryOutputType>: Sized { /// The type from the original type constructor that also has this residual type, /// but has the specified Output type. type TryType: Try<Output = TryOutputType, Residual = Self>; } }

With corresponding

照応する
simple implementations
実装
like these:

#![allow(unused)] fn main() { impl<T> GetCorrespondingTryType<T> for Option<!> { type TryType = Option<T>; } impl<C, B> ops::GetCorrespondingTryType<C> for ControlFlow<B, !> { type TryType = ControlFlow<B, C>; } }

And thus

それゆえに、従って、
allow
許可する、可能にする
code to put whatever value they want into the appropriate type from the same family.

This can be thought of as the type-level inverse

逆の
of Try's associated types: It splits them apart, and this puts them back together again.

(Why is this not written using Generic Associated Types (GATs)? Because it allows

許可する、可能にする
implementations
実装
to work with only specific
特定の
types, or with generic-but-bounded types. Anything using it can bound
制限する、結び付けられて
to just the specific
特定の
types needed for that method.)

A previous

前の
version of this RFC included a trait along these lines, but it wasn't needed for the stable-at-time-of-writing scenarios. Furthermore, some experiments demonstrated that having a bound
制限する、結び付けられて
in Try requiring it (something like where Self::Residual: GetCorrespondingTryType<Self::Output>) wasn't actually even helpful for unstable scenarios, so there was no need to include it in normative section
of the RFC.

Possibilities for try_find

Various

さまざまな
library methods, such as try_map for arrays
配列
(PR #79713), would like to be able to do HKT-like things to produce
産出する
their result
結果、戻り値
types. For example, Iterator::try_find wants to be able to return a Foo<Option<Item>> from a predicate that returned a Foo<bool>.

That could be done with an implementation

実装
such as the following:
下記の、次に続く、追従する

#![allow(unused)] fn main() { fn try_find<F, R>( &mut self, f: F, ) -> <R::Residual as ops::GetCorrespondingTryType<Option<Self::Item>>>::TryType where Self: Sized, F: FnMut(&Self::Item) -> R, R: ops::Try<Output = bool>, R::Residual: ops::GetCorrespondingTryType<Option<Self::Item>>, { #[inline] fn check<F, T, R>(mut f: F) -> impl FnMut((), T) -> ControlFlow<Result<T, R::Residual>> where F: FnMut(&T) -> R, R: Try<Output = bool>, { move |(), x| match f(&x).branch() { ControlFlow::Continue(false) => ControlFlow::Continue(()), ControlFlow::Continue(true) => ControlFlow::Break(Ok(x)), ControlFlow::Break(r) => ControlFlow::Break(Err(r)), } } match self.try_fold((), check(f)) { ControlFlow::Continue(()) => Try::from_output(None), ControlFlow::Break(Ok(x)) => Try::from_output(Some(x)), ControlFlow::Break(Err(r)) => <_>::from_residual(r), } } }

Similarly,

同様に
it could allow
許可する、可能にする
Try to automatically
自動的に
provide an appropriate map method:

#![allow(unused)] fn main() { fn map<T>(self, f: impl FnOnce(Self::Output) -> T) -> <Self::Residual as GetCorrespondingTryType<T>>::TryType where Self::Residual: GetCorrespondingTryType<T>, { match self.branch() { ControlFlow::Continue(c) => Try::from_output(f(c)), ControlFlow::Break(r) => FromResidual::from_residual(r), } } }

Possibilities for try{}

A core problem with try blocks as implemented

実装する
in nightly, is that they require their contextual type to be known.

That is, the following

下記の、次に続く、追従する
never compiles, no matter the types of x and y:

#![allow(unused)] fn main() { let _ = try { foo(x?); bar(y?); z }; }

This usually isn't a problem on stable, as the ? usually has a contextual type from its function, but can still happen there in closures.

But with something like GetCorrespondingTryType, an alternative

代わりのもの、選択肢
desugaring becomes available which takes
とる
advantage of how the residual type preserves the "result-ness" (or whatever-ness) of the original value. That might turn the block above into something like the following:
下記の、次に続く、追従する

#![allow(unused)] fn main() { fn helper<C, R: GetCorrespondingTryType<C>>(r: R) -> <R as GetCorrespondingTryType<C>>::TryType { FromResidual::from_residual(h) } 'block: { foo(match Try::branch(x) { ControlFlow::Continue(c) => c, ControlFlow::Break(r) => break 'block helper(r), }); bar(match Try::branch(y) { ControlFlow::Continue(c) => c, ControlFlow::Break(r) => break 'block helper(r), }); Try::from_output(z) } }

(It's untested whether the inference engine is smart enough to pick the appropriate C with just that -- the Output associated type is constrained to have a Continue type matching

一致する、マッチさせる
the generic parameter,
仮引数
and that Continue type needs to match
一致する、マッチさせる
that of z, so it's possible. But hopefully this communicates the idea, even if an actual
実際の
implementation
実装
might need to more specifically
特に
introduce
導入する
type variables
変数、ストレージ
or something.)

That way it could compile so long as the TryTypes of the residuals matched.

一致する、マッチさせる
For example, these uses in rustc would work without the extra annotation.

Now, of course that wouldn't cover anything. It wouldn't work with anything needing error conversion,

変換
for example, but annotation is also unavoidable in those cases -- there's no reasonable way for the compiler to pick "the" type into which all the errors are convertible.

So a future RFC could define

定義する
a way (syntax, code inspection, heuristics, who knows) to pick which of the desugarings would be best. (As a strawman, one could say that try { ... } uses the "same family" desugaring whereas try as anyhow::Result<_> { ... } uses the contextual desugaring.) This RFC declines to debate those possibilities, however.

Note that the ? desugaring in nightly is already different depending whether it's inside a try {} (since it needs to block-break instead of return), so making it slightly more different shouldn't have excessive implementation

実装
cost.

Possibilities for yeet

As previously mentioned, this RFC neither defines

定義する
nor proposes a yeet operator.
演算子
However, like the previous
前の
design
設計(する)
could support one with its Try::from_error, it's important that this design
設計(する)
would be sufficient to support it.

yeet is a bikeshed-avoidance name for throw/fail/raise/etc, used because it definitely won't be the final keyword.

Because this "residual" design

設計(する)
carries along the "result-ness" or "option-ness" or similar,
似ている、同様の
it means there are two possibilities for a desugaring.

  • It could directly
    直接
    take
    とる
    the residual type, so yeet e would desugar directly
    直接
    to FromResidual::from_residual(e).
  • It could put the argument
    引数
    into a special residual type, so yeet e would desugar to something like FromResidual::from_residual(Yeeted(e)).

These have various

さまざまな
implications -- like yeet None/yeet, yeet Err(ErrorKind::NotFound)/yeet ErrorKind::NotFound.into(), etc -- but thankfully this RFC doesn't need to discuss those. (And please don't do so in the GitHub comments either, to keep things focused, though feel free to start an IRLO or Zulip thread if you're so inspired.)