[Pre-RFC] Inferred member

The first pre-RFC for a similiar feature was published by @Fishrock123 at [Pre-RFC] Inferred Enum Type. I suppose it was not well accepted due to readability or language confusion (.M, _::M). I will try using the reserved word enum as a path prefix, as in enum::M.

Summary

Support for the path enum::M, indicating a member M of an inferred enum or struct.

Throughout this proposal, enum interchangeably refers to enums and structs (including bitwise sets designed through the bitflags crate).

Motivation

This path syntax allows shortening the reference or initialization of enum members. When using this syntax, it is not required to:

  • import the enum or the enum's member into scope;
  • fully qualify the enum's member

Guide-level explanation

It is possible to refer.to a constant, unit, tuple, or struct variant using an inferred enum member path in the form enum::M.

enum MyEnum {
    MyVariant1,
    MyVariant2(f64),
    MyVariant3 { x: f64 },
}
let value: MyEnum = enum::MyVariant1;
let value: MyEnum = enum::MyVariant2(10.0);
let value: MyEnum = enum::MyVariant3 { x: 10.0 };

enum::M paths may be useful for avoiding including a library's variant or enum into scope:

use restaurant::FoodCategory;

enum::M paths may be useful when constructing nodes:

enum::ExpressionStatement(with! {
    expression: enum::NumericLiteral(10.0),
})

Reference-level explanation

  • The enum::M path is allowed within
    • Matching patterns
    • Unit variant usages (enum::M)
    • Tuple variant usages (enum::M())
    • Struct variant usages (enum::M {})
  • The enum::M path resolves either to a variant of an inferred enum or a constant from an inferred struct. It is a compile-time error if the expression has no expected enum, that is, a type annotation is required.
  • Syntax ambiguity: an expression slightly conflicts with items due to the enum:: path. They are disambiguated when an enum item starts with the enum token and the immediately following token is either :: or an identifier.

Drawbacks

It is not clear whether the proposal resolves the problem of long enum paths. Compared to the first proposal, this adds extra three characters to the path; however it adds the same benefits from the other proposal (scope and conciseness).

Other drawbacks:

  • r#enum::Variant is already allowed, meaning Variant from module enum

Rationale and alternatives

The first proposal suggested a _::M form instead of enum::M; compared to that proposal, this proposal does not use a punctuator and resembles module paths such as crate, super and self. The advantage of this proposal is readability and the disadvantage is more three keystrokes effort for the programmer.

Prior art

ActionScript 3 has received a proposal for simple enums and compile-time implicit conversion from string literal to enums where applicable:

enum MyEnum {
    const MEMBER_1;
}
const value: MyEnum = "member1";

The same cannot be applied to Rust due to naming conventions and also due to tuple and struct variants:

let value: MyEnum = "MyVariant1";
let value: MyEnum = "MyVariant2"(10.0);
let value: MyEnum = "MyVariant3" { x: 10.0 };

Unresolved questions

N/A

Future possibilities

N/A

A drawback that isn't mentioned is that r#enum::Variant is already allowed syntax, in this case it could refer to type Variant in module enum.

This isn't likely to be the case, because enum is a keyword, but it is legal in stable Rust.

1 Like

One thing I appreciate about Swift’s equivalent feature (which admittedly was added to the language much earlier) is that it is not limited to enums. Even if you find it questionable to use it for method calls returning Self, I still think it would be appropriate for Self-type consts, like usize::ZERO or Duration::SECOND. Using enum as part of the syntax seems to rule that out before it even gets proposed.

Hmm, I was aware of Swift being used for bitwise sets:

I am wondering then if we can use enum:: for constants and bitwise flags?

I have updated the RFC to allow enum::M to refer to constants (const) as well. This should almost match Swift's static inference form, excluding method calls as pointed by @jrose

Whether it's enum::Yes or _::Yes or .Yes doesn't do anything to address the concern that today enum variants are often named in ways that are completely clear -- like Mutability::Not -- when named today because the enum name is included, but that would be confusing -- like foo(enum::Not) -- with just the variant.

1 Like

In TypeScript it is pretty common to use enumerations based on unions of string literal types.

There are a number of things that are down to style. For example, I don't particularly like trees of ::{…} on a single use statement and prefer to only use {} at the leaf and then a new use statement otherwise. I think that an ignore-by-default clippy (or rustc I suppose, but this feels more clippy-like to me) lint to say "do not allow enum:: inference" would exist for things like that.

1 Like

Sure, but I would expect that it'd be literals like "IgnoreCookies" -- like how C enums variants are named -- not "No" -- like how Rust enum variants are named.

Would your concern be addressed by the fact that people who care about readability won't use the short form when it's unclear for a particular enum? I mean, otherwise you'd be arguing to never support inference-based-abbreviations because there exist enums for whom they don't work well.

4 Likes

It’s very context-dependent, too. If you’re initializing struct fields, the type is probably redundant:

Variable {
  mutable: _::Not,
}
4 Likes

I think this can be solved with a sufficiently clever lint:

  • Blacklist a set of known meaningless-out-of-context words
  • Make the list configurable in clippy.toml
  • Separate lint for banning the use altogether

This is such a useful feature that the readability penalty for misuse doesn't seem bad at all in comparison.

2 Likes

I've written about this in more detail in other threads before, but I want to emphasize that it's not just that there exist enums named like this, but that given current Rust it's entirely reasonable to name enums like that. (It's not like "well this is only bad if you did struct u32(String);" or something.) AFAIK the normal guidance is not to have Mutability::NotMutable, and the people who made enums like that to avoid foo(true, false) APIs are the ones most likely to be annoyed with foo(.Yes, .No).

I agree it would often be handy to be able to infer variant types. I just don't know how to weigh the implied "oh, and please rename lots of enums in the ecosystem to work well with this" consequence of having this feature.

A detailed write-up in the Rationale & Alternatives section about why this is ok and how concerns will be addressed is what I want to see for a Pre-RFC in this space. When exactly can you do this, if it's discouraged sometimes how do lints detect that, why is that the correct set of trade-offs for the feature, etc.

A new syntax option isn't going to unblock it. That's not the problem.

4 Likes

Some prior art for the lint would be when editors choose to show inline hints for parameter names.

I can see the lint being something like "must include type name unless it's mentioned in the call/assignment lhs", eg.

let mutable = Mutable::Never;
// default allow lints to be shortened to:
let mutable = . Never;

// but
let lhs = . Never
// default warn lints with fix to be expanded to
let lhs = Mutable::Never;

Though there's some obvious issues w/ pluralizing and determining how much of the lhs to look at (eg "looking through" derefs, unwraps, .0 and .value type things probably makes sense, but probably not past indexing a collection)

2 Likes

This topic’s OP has decided they wanted to close this topic.