[Pre-RFC] Optional language feature for inferred variants

Summary

Introduce optional language-level feature that allows writting SomeVariant versus SomeEnum::SomeVariant where SomeEnum is expected. For this, one must configure the package Cargo.toml with the following:

[language-features]
inferred-variants = true

As outlined later in this document, starting from a specific Rust edition, depending on community decisions, this feature could be enabled by default.

While some say enabling this may break existing code, the chance to break is mostly impossible, as covered in the section Reference-level explanation. Regardless, it is an optional feature.

Another thing to add is, Cargo's cargo init or cargo new commands may automatically generate this option as true.

Motivation

This aims at reducing enum qualifier verbosity in any context. This allows to omit workarounds such as use SomeEnum::*; and use SomeEnum as Se;;

Guide-level explanation

According to the configuration of the current package, where language-features.inferred-variants = true:

  • Given a lexical reference with an expected enum type E: if it matches the name of any of the variants from E, it results in such variant; otherwise the default reference resolution takes place for the lexical reference.

Reference-level explanation

A reference to an enum variant can be written in different ways depending on the context:

enum SomeEnum {
    SomeVariant,
}

fn some_fn(et: SomeEnum) {
}

some_fn(SomeEnum::SomeVariant); // full
some_fn(SomeVariant); // inferred

With this language feature, a lexical reference may incorrectly refer to an enum variant where the user may desire to refer to any item else with the same name, but this only happens where an enum is expected. This can be referred to as shadowing.

While shadowing problems are hard to occur, since they only happen where a specific enum is expected, the workaround to refer to something shadowed by an inferred enum variant is to use an use ...; statement. For example:

enum E { Bar, }
use xns::Bar as TBar;

To re-enforce: unwanted lexical shadowing is almost impossible to occur, because the inference only takes place where the enum is expected. Thus, one more example that illustrates how that is almost impossible to happen:

struct S;
enum E { S, }
let v: S = S;
let v: E = S;

More examples: the expression Foo::x() never necessarily resolves to a variant Foo, because this is a member expression and Foo is the base object and x is the member. Such expression can be resolved expecting some enum, however it is a member expression.

To re-enforce (2): aware of the lexical mechanism, enabling this feature on an existing project/package is almost impossible to break the code, even if a lexical item has the same name as an expectable variant.

Drawbacks

N/A

Rationale and alternatives

  • Ideas such as _::SomeVariant seem to be noisy:
// MyOwnResult
some_fn(Ok(v));
some_fn(_::Ok(v));

match r {
    Ok(v) => ...,
    Err(reason) => ...,
}

match r {
    _::Ok(v) => ...,
    _::Err(reason) => ...,
}
  • Not having this feature is a lack of opportunity for future adoption. For example, starting from an edition year, this can become a feature enabled by default.

Prior art

  • Other languages have the feature in different forms:
    • Haxe: var sv:SomeEnum = SomeVariant;
    • Swift: let sv: SomeEnum = .SomeVariant;
  • Some users may expect this to be true by default, at least starting from a particular Rust edition.
  • The feature is simple to implement.

Unresolved questions

N/A

Future possibilities

N/A

How is showing that something is inferred "noisy"?

One thing worth noting is that language features don't get enabled by Cargo.toml. They have to occur within the Rust file, as cargo isn't even required to be used.

Also, where are you getting the 3% chance of breakage from?

6 Likes

Some user opinions on related topics react that the _::Xxx syntax seems weird.

One thing worth noting is that language features don't get enabled by Cargo.toml . They have to occur within the Rust file, as cargo isn't even required to be used.

Both Rust (the compiler, rustc) and Cargo will have to be touched to implement the optional language feature.

Also, where are you getting the 3% chance of breakage from?

Just an estimation. I wanted to mean it's nearly impossible.

In respect to _::Xxx, I added this comparison:

// MyOwnResult
some_fn(Ok(v));
some_fn(_::Ok(v));

match r {
    Ok(v) => ...,
    Err(reason) => ...,
}

match r {
    _::Ok(v) => ...,
    _::Err(reason) => ...,
}

Do you mean that it is using type information to detect when the enum is expected? This is impossible with the current compiler architecture (name resolution takes place before type checking), and I highly suspect will require a fixed-point algorithm to both name resolution and type checking, which is very bad.

Rust intentionally tries to avoid "splitting the ecosystem" into different language variants. A more typical suggestion is to change behavior over an edition boundary.

The RFCs that come to mind which made claims about the level of breakage first created an implementation, and then ran crater to estimate the impact. Just say you anticipate breakage to be low if you can't back it up.

5 Likes

By what you're saying, then even _::Xxx is impossible to implement, as I understand, since it's based on the expected enum type. Is that the reason why the previous proposal wasn't accepted?

This is different because during name resolution we know _ refers to some inferred enum so we can defer its resolution to type checking phase. But when we got a plain name, we have to resolve it and we can't know whether it'll be a variant or a type.

1 Like

Things like this have been discussed many times, going back years. Given that it hasn't already happened, any successful RFC will need substantial motivation and rationale sections. There're plenty of previous concerns to address.

One alternative, then, would be to use a different marker. For example, there's the Swift-inspired .SomeVariant, as I've mentioned before (Allow wildcard while destructuring structs - #11 by scottmcm).

Having no marker needs much stronger rationale, IMHO, as it loses the "when I see a name outside a method call I know I can find the use or definition for it in the file".

I suspect that this, for an individual feature, isn't going to happen. You opt into editions, sure, but not to individual features. That way there's a very small number of dialects. It's absolutely essential to avoid a combinatorial explosion, both for the test matrix and for human understandability. If this got its own opt-in flag, then every future contentious feature would propose adding them too.

Ideally, assuming it's a desirable feature, it'd be turned on in all editions when stabilized. (As that's the default preference for all new features.) You say that unwanted behaviour here is "almost impossible" -- does that mean that it's possible for this to be a breaking change, and thus could only be enabled over an edition? Or is there a way it could be defined to not be breaking? And if so, what implications does that have for inference stability over the addition of extra items in the future?

6 Likes