Why is <Option as Try>::Residual not a simple unit struct?

So I was look at implementation of Try for Option and noticed that the Residual type is this weird Option<Infallible> type, which looks needlessly complicated.

From what I understand, it should be a unit struct, the unit itself () would be most clean, but a new unit struct, like for example:

struct OptionResidual;

could also work.

The only advantage of Option<Infallible> that I can think of is that it implements Try, but I don't see how that could be important, especially since the type in the Error variant in Result doesn't have to do that.

EDIT:

Result actually does that as well, with <Result<T, E>::Residual being <Result<Infallible, E>>.

@scottmcm will no doubt chime in, but in the meantime the "Note to implementors" of Try::Residual discusses this:

The choice of this type is critical to interconversion. Unlike the Output type, which will often be a raw generic type, this type is typically a newtype of some sort to "color" the type so that it's distinguishable from the residuals of other types

This is why Result<T, E>::Residual is not E, but Result<Infallible, E>. That way it's distinct from ControlFlow<E>::Residual, for example, and thus ? on ControlFlow cannot be used in a method returning Result.

If you're making a generic type Foo that implements Try<Output = T>, then typically you can use Foo<std::convert::Infallible> as its Residual type: that type will have a "hole" in the correct place, and will maintain the "foo-ness" of the residual so other types need to opt-in to interconversion.

So the Residual has to be a bespoke type to make conversions work. If I understand correctly, struct OptionResidual would also work, but Option<Infallible> (hopefully spelled Option<!> in the future) is already there, and in a way the natural, "correct" type to answer the question "What is left when you subtract T from Option<T>?" (If enum variants were types, then I guess the residual could simply be the None type.)

5 Likes

Option<Infallible> is a 1-ZST

[src/main.rs:4] Layout::new::<Option<Infallible>>() = Layout {
    size: 0,
    align: 1 (1 << 0),
}

that's conveniently constructible from the token None.

And thus it serves as a unit struct in essentially all the ways that matter.

Could it be a separate type? Certainly. But then we'd have to argue about bikeshed its name, whether it should have any methods, which traits it should implement, etc. That might be worth doing if this were something that people would need to mention frequently, but when it's a usually-hidden detail, it's not clear that it'd be helpful.

From https://rust-lang.github.io/rfcs/3058-try-trait-v2.html#the-try-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.

1 Like

That would also depend on how they would be implemented. Option::<i32>::None could be different than Option::<i64>::None, in which case you're back at the problem of what T to use.

4 Likes

I think the point is that the residual for Option<T> would be Option<T>::None; the residual for Result<T, E> Result<T, E>::Err.

This works (assuming the existence of enum variants as types) but isn't really a great solution for a couple reasons:

  • While it works for 2-variant enums, it doesn't work well for 3-variant enums pulling one variant as the continue case, 2 as the break, since it's a separate feature to have a type which is one of multiple variants. Even with e.g. pattern types, current drafts of what the feature would look like have pattern types behave similarly to subtypes of the full type.
  • The current drafts for enum variants as types pretty much all agree that they should be the same size as the full enum, just with the other variants made illegal. Option<!> is basically for<T> Option<T>::None except without the likely desirable subtype relationship.
1 Like

And also without causing extra monomorphization.

In a -> Option<String> function, you want ? to all hit the same Option<String>: FromResidual<Option<!>>. If Option<T>'s residual was Option<T>::None, then you'd end up with it potentially needing to monomorphize Option<String>: FromResidual<Option<i32>::None> and Option<String>: FromResidual<Option<bool>::None> and …

4 Likes

Honestly the biggest problem is that the name Infallible only makes sense in the context of the Result error type. If it was the Typescript name Never then Option<Never> reads pretty clearly as what the meaning is (to me)

8 Likes

You'll notice that ! is called the "never type".

The goal is to stabilize ! and make Infallible just a type alias for it at the same time. At which point these error messages will be Option<Never> or Option<!>, which will hopefully be easier to read.

EDIT: wow, it's my 6th anniversary of joining IRLO, says the little pie icon.

6 Likes

Interesting catch. Is this the situation you are talking about?

fn mby_string() -> Option<String> {
    let mby_number = Some(5i32);
    Some(mby_number?.to_string())
}

Option<String> needs to be constructed from Option<i32>::Residual, which is how the ? works (if it "fails", you have the residual that is converted using this method to the return type).

So you would need something like this?

impl<T, S> FromResidual<Option<T>::None> for Option<S> {
    fn from_residual(_residual: R) -> Option<T> {
        None
    }
}

Pretty wild. But residual type could be Option<()>::None, removing this problem. Not sure if that is better than Option<Infallible>, both are equally bad IMO, so why not stick with what we have now.

Option<Infallible> guarantees that the Some variant will never be constructed; even if that doesn’t really matter for this sort of autogenerated code, it also means the whole enum becomes a zero-sized type (because there is only one possible state). Of course, it’d be optimized away completely in a release build, but even so using Infallible is better on both theoretical and practical grounds.

2 Likes

Depends on how the hypothetical Option<()>::None subtype would be implemented. I would actually prefer if it had the same size and layout as the original Option<()> so yea, in that case, it wouldn't be zero sized like Option<Infallible> is.

1 Like

And it being a ZST is helpful because the desugaring involves ControlFlow<Residual, Output>. And if the output is NonZeroU32, then that ControlFlow optimizes down to being just an i32 (not guaranteed) when the residual is a ZST, simplifying the intermediate values. (For example in NonZeroU32::new(x)?.)

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