Enum path inference with `_Variant` syntax

With some regularity there appears proposals to introduce _::Variant or .Variant shorthand for path::Enum::Variant analogous to Swift's .variant syntax. I've also seen :Variant and ::Variant and other similar ideas e.g. to allow use path::Enum::Variant::* at the top of match scope, or to just implicitly glob-import enum variants into patterns. The syntax looks more or less like this:

match foo {
    _::Bar => baz,
    _::Qux => quux,
}

Undoubtedly Swift set a good precedent with this feature, their community seems to embrace it a lot, and having something similar is certainly desired by many in Rust community.

But all of these proposals then becomes abandoned. I think that's mostly because our language is quite different and it's impossible to recreate in Rust as swift experience as with .variant in Swift. For example, the closest might have been .Variant but even the smallest detail like an uppercase letter on it becomes a surprisingly huge obstacle: the beginning of this construct ceases to look as beginning of pattern or expression — it's something new instead and that may not fit into Rust's strangeness budget. Also, as far as I remember community never had been able to reach consensus whether this syntax should be available in patterns, in expressions, or in both, and either whether it should be available for structs, for enums, or for both; — it was determined that technically it's possible to implement whatever combination we want but also it was always hard to motivate any particular.

New idea

I propose to reuse _Variant syntax.

Yes, I know that this is already valid identifier, and that it differs from previous proposals only in one symbol which may not look significant. But the premise here is that we already have this syntax in Rust, and we already have underscore to mean "elide" or "hide", and we already have a brilliant precedent of introducing new feature through existed syntax with .await, and we rarely use types starting with underscore anyway.

So, within the current proposal it should work as this:

  • Compiler still continues to accept types, variables, constants, etc. with names starting with underscore
  • Then when writing _Something and matching name is already in scope it's picked first
  • Otherwise, if it's not in scope then compiler tries to infer enum type and pick its corresponding variant
  • And if no enum or variant were found [assuming in pattern context] then a refutable binding is introduced as a fallback, simultaneously compiler issues warning about an uppercase letter in variable name

There's in fact only one user facing addition: to make compiler try pick enum variant first before creating refutable binding or emitting missing type/variable error. And it seems to be fully backward compatible except one case where _X refutable binding in old code resolves in a new code to an enum that also contains _X variant. Fortunately, that's an unlikely scenario and it could be very easily mitigated on edition boundary by simply renaming _X into something else. That said, a new edition would be also required but most likely it would be just a formality.

Examples in pattern context

pub fn is_enabled(&self) -> bool {
    matches!(self, Self::Enabled)
}

pub fn is_enabled(&self) -> bool {
    matches!(self, _Enabled)
}

pub fn is_enabled(&self) -> bool {
    matches!(self, .Enabled)
}

pub fn is_enabled(&self) -> bool {
    matches!(self, _::Enabled)
}

// from internals.rust-lang.org/t/bring-enum-variants-in-scope-for-patterns/12104
let is_timeout = match error {
    LibraryError::FailedRequest(RequestError::(ConnectionError::Timeout)) => true,
    _ => false,
}

let is_timeout = match error {
    _FailedRequest(_ConnectionFailed(_Timeout)) => true,
    _ => false,
}

let is_timeout = match error {
    .FailedRequest(.ConnectionFailed(.Timeout)) => true,
    _ => false,
}

let is_timeout = match error {
     _::FailedRequest(_::ConnectionFailed(_::Timeout)) => true,
     _ => false,
}

To make it clear once and for all: this syntax is for enums only and not for structs or anything else. At first, that would be familiar mental model for Swift users which IMO is important to preserve since both languages seems to copy features from each other and usually are expected to behave similarly. And at second, in this way we can be certain that nobody would become lost in long patterns common in Rust code when destructuring structs like this:

let _ {        // Some lookahead is required to understand
    app_id,       // what we're trying to destructure here
    window_properties,
    rect: _ {
        mut x,
        mut y,
        width,
        height,
        ..
    }
    ..
} = focused_window;

let swayipc::Node {  // This pattern instantly makes sense
    app_id,
    window_properties,
    rect: swayipc::Rect {
        mut x,
        mut y,
        width,
        height,
        ..
    }
    ..
} = focused_window;

And despite patterns is where path inference is desired the most I also propose to make it available in expressions either — exactly how Swift developers implemented it. IMO the symmetry between patterns and expressions is the reason to have this syntax being "activated" by operator on a first place; since anything like _Variant or .Variant would parse fine in both contexts why should we artificially restrict it to a particular? Moreover, since Some(x) already looks the same in patterns and expressions having path inference behaving differently for many people could also appear surprising or even frustrating.

Examples in expression context

let piped = !atty::is(Stream::Stdin);

let piped = !atty::is(_Stdin);

let piped = !atty::is(.Stdin);

let piped = !atty::is(_::Stdin);

let state = if !finished {
    state::Progress::Continues
} else {
    state::Progress::Completed
};

let progress_state = if !finished {
    _Continues
} else {
    _Completed
};

let progress_state = if !finished {
    .Continues
} else {
    .Completed
};

let progress_state = if !finished {
    _::Continues
} else {
    _::Completed
};

Real world example comparison

I've extracted some snippet (not being written by me) from rustfmt to demonstrate how the proposed syntax and some alternatives may look when there's more context around. This was put on image because the same comparison looks terribly in markdown and even worse on Discuss which puts scrollbars on long chunks of code.

Click on image to enlarge:

So, this is where advantage of _Variant may become actually visible:

  • The current Rust syntax provides more useful information but its problem is that there's a lot of that information and it repeats a lot, moreover, it's not about how visible code works but how containing it code base is organized. When reading code in most of cases I simply not interested in that while for the rest an IDE feature like inlay hints which brings explicit paths back seems to be sufficient. Also, I feel that this information has "compressing" effect on how variables are named e.g. we write match rx.recv() { path::Msg::Variant => ... } while instead it should be match what_messages.recv() { _Variant => ... } — might be a bit obscure but I hope the idea is obvious. Overall, this is a good looking syntax but hard to read without distractions.
  • The .Variant alternative is rather okay but there's already a lot of dots in Rust code especially in :: and .. and that makes this syntax a bit hard to discern. Also, problematic is the fact that a single dot usually means "field or method access" so with that match expressions looks almost like method chains and some effort is required to perceive them as control flow constructs (Swift doesn't falls into that because it puts case before each arm). IMO, disambiguation from method call here even for the compiler might be too complicated task. In contrast, underscores in Rust are semantically attached not to preceding item but to surrounding them scope e.g. that's visible on match x { _ => .. }; and for enum path inference this has a lot of sense especially in expression context e.g. from the above snippet self.visit_attrs(&item.attrs, _Outer) reads as a mini DSL where _Outer could be perceived as a continuation of what visit_attrs started to communicate. BTW, also interesting is the fact that underscores in _Variant beautifully aligns with _ if and _ => in match expression while . doesn't.
  • The _::Variant alternative is bad because of noisiness and either because here underscore represents some "elided away" namespace common for everything. I also feel that the :: is the source of that noisiness, that it's completely unnecessary and that it's either misleading because it looks like a part of path which wasn't completely elided away. So, for user this might suggest that things like _::path::Enum::Variant or _::Struct {} are supported as well or that we have plans to introduce them in the future (I'm skeptical that we would have them). Furthermore, such complicated structure might be annoyingly hard to edit and navigate e.g. it takes three Ctrl+Backspace clicks in order to delete it while only one is required with _Variant.

Drawbacks

There are of course many, but I don't believe anything is critical.

The most problematic might be C/C++ FFI since a lot of names in these languages begins with underscore and interoperability with them is a priority in Rust. Fortunately, from what I know in both languages users shouldn't define their own types beginning with underscores — these are internal for compiler/tooling implementation and usually in Rust we would find them in very low level code e.g. in core::arch module. Also luckily for us there doesn't seem to be any _UpperCamelCase but mostly _snake_case and _UPPER_CASE names — that's of course subtle but nevertheless a distinction on which e.g. syntax highlighters could rely.

It's also possible to argue that currently Rust allows prefixing unused types with underscore in order to stop compiler complaining about them — this is derived from the same feature for variables. Indeed there seems to be conflict, but I think we can live with it considering how uncommonly types are prefixed with underscores and that #[allow(dead_code)] together with infinity of other naming options allows to achieve exactly the same result.

Perhaps there remains some value in having _Name being a valid type e.g. when experimenting in Rust playground, but I also think that this use case isn't important enough to force us selecting less optimal syntax for enum path inference or completely abandon it. Anyway, that code will still continue to compile, only it might become less idiomatic and the effect of underscore prefix in type names for users might become harder to discover.

What would be objectively bad is the visual similarity between ignored variables and inferred enum paths e.g. when _binding is placed alongside with _Variant in match expression these could be very easily confused between. But it also seems that common reason, syntax highlighting and compiler warnings (e.g. to prevent the aforementioned situation in match) would mitigate this issue completely so on practice any confusion would rarely occur.

What if we also append enum name suffix?

The constant source of criticism of enum path inference was always that it would inevitably lead to "disorientation" because less specific variant names like _Delete, _Enabled, _Stop could belong to different enums coexisting in the same code base e.g. {LocalFile|RemoteFile}::Delete, {DarkTheme|Cookies}::Enabled, {EventLoop|ServerConnection}::Stop and in some circumstances it may be too complicated to determine whose exactly enum variant we reading.

Somebody have said that this may change the way how we name enum variants e.g. LocalFile::Delete may become LocalFile::DeleteLocalFile, Cookies::Enabled may become Storage::CookiesEnabled, EventLoop::Stop may become EventLoopMessage::MessageStop and so on (unfortunately I've lost link to that discussion). So, this is very solid objection: despite enum renaming alone may be too disruptive we also like the way how currently enums are named and nobody would want to change it.


We can address this, although the way which I propose is very surprising: enum path inference when applied could also force users to append enum name to the variant as suffix. For example, there would be _DeleteRemoteFile, _EnabledDarkTheme, _StopServerConnection and so on instead. I know, this looks weird and perhaps everyone thinks at this point that I've completely lost my mind — what's the point in this syntax when in most of cases it would basically change word ordering resulting in the same amount of symbols?

But this actually seems to solve every problem with path inference for enums:

  • Module names on enums would be hidden but not enum names that contains the most useful information
  • The construct is monolithic: no parts, no special symbols, so it should be easier to edit and navigate
  • More intuitive word ordering for humans that either matches the typical for Rust value: Type pattern
  • Encourages users to uniformly access enums and name them sensibly
  • Almost no chances that this will require changes to someone's code in a new edition
  • Enum patterns and instantiations are very easy to discern because this syntax is much "heavier"
  • It's also self-obvious that this will support enums only and there's no way to enable it on structs

Below is the same image with this and .VariantEnumName alternatives added:

IMO, this effect is very interesting but I also wonder whether it might be generalized on practically every enum or sometimes an utter nonsense would be still produced. Preliminarily I've ran some experiments on files in rustfmt and in some other crates and so far found only that in some cases it may lead to repetitions like _FnFnKind on the above example and that particular variants may become incredibly long e.g. _ScrollContinuousPointerEvent. Anyway, readability of code seems to be improved and these corner cases don't even look that bad.

Real downsides are that perhaps learnability of the language would become worse a bit, and that overall strangeness budget may not welcome such contribution. People either may not perceive it well because advantages of writing code in such way may not be instantly obvious. And of course it's extra work and an extra maintenance burden for language and tooling developers.


So, should I prepare RFC for this feature or it's too strange? Are there people willing to help me with that?

I don't think using _Variant instead of _::Variant or .Variant really solves anything. Having it be ambiguous with a no-unused-variable-warning binding seems worse, to me.

To me, the blocker here has not been syntax, but the naming thing (which you did mention)

The syntax we could just pick one of the possible answers, if we really wanted.

But if .Yes could be AllowUnstable::Yes or Capturing::Yes or IsPattern::Yes, that's a more fundamental concern that's harder to resolve. I don't really know how to make progress on it.

11 Likes

One intermediate (which I don't really like) is to "just" allow Enum::Variant to work if Enum is the correct name but no namespace with that identifier is in scope.

But in that case it still seems reasonable to provide a machine-applicable auto import rather than changing the language to skip having the import line.

I'm mostly of the opinion that this elision is a fundamental part of API design, and thus should either be present from day 0 or not added just for the purpose of eliding enum imports/names.

(I do still support more general elision of inferrable types, though, which would apply in this position still. So if that makes you dismiss my position as incompatible with itself, so be it. It's a nuanced opinion about tradeoffs.)

Ultimately, @160R, I think you value textual brevity and "cleanliness" (which for you seems to be inversely correlated to the amount of punctuation) moreso than the average Rust developer/advocate here, which more prefer localized clarity of intent/semantics and limiting churn without inalienable benefit.

(Also, it's just always better to include text rather than images of text. If you want to prevent samples from taking up too much space in a post, use a <details> element. If syntax coloring breaks, use ```text; if syntax coloring is a requisite to understanding the example, then an image can be additionally included, but maybe reconsider using IDE features as a required communication channel, rather than a redundant one. Redundancy is actually a good thing in communication; it helps both confirm accuracy (by catching ill formed contradictions) and to ensure that information is readily available.)

2 Likes

I did like the idea (which might have been yours) that inference is already essentially allowed in irrefutable places, so that might be one way to make progress on this -- but that wouldn't apply for enums at all, so probably doesn't help OP.

Come to think of it, we could at least improve the diagnostic here if people try it: Suggest the correct name if `_` is used in a struct literal · Issue #98282 · rust-lang/rust · GitHub

1 Like

I'm going to be writing an RFC for path elision in patterns in the future: Name elision

It's not at the top of my list, but it is on it!

2 Likes

How about some kind of alias/shortcut attribute on variants?

enum AllowUnstable {
   #[alias = "Unstable"]
   Yes,
   #[alias = "Stable"]
   No,
}

which would then enable use of _::Stable and _::Unstable.

That's an interesting thought. I guess that would just be for the .Stable/.Unstable?

It's hard for me to weigh the extra language surface of that vs just doing something like this, though

pub use AllowUnstable::{Yes as Unstable, No as Stable};

(Perhaps in a prelude-ish module or something.)

1 Like

I still haven't seen anyone explain to me why this _::Stable, _::Unstable is somehow better than this

use AllowUnstable as U;

U::Stable, U::Unstable

The cognitive cost of a single rename at the start of the function (or maybe even a module) is significantly less than trying to infer the missing parts of the path in your head.

It's not like anyone is likely to write enum-variant-polymorphic code which has explicit names of variants, but the enum types themselves are to be inferred, potentially differently depending on the context.

4 Likes

The difference is availability.

With _::Yes, you know that this is the standard way of eliding the enum name, and that this is the Yes variant of the function argument enum type.

Alternatively generalized a bit, but still using the exact same logic:

With _::Yes you know that this is the standard way of eliding the function argument type, and that this is the Yes associated constants of the function argument type.

Worth making explicit here: this also requires that the function argument type is Enum and not some inference variable like impl Into<Enum>... this would make generalizing function argument types into a more breaking change than it is currently, but still an allowed inference failure.

For the same reason, unless its macro emitted code, this precludes enum polymorphic code.

With U::Yes, you have the Yes associated constant of some namespace U. This could be an alias of the function argument type, or it could be an unrelated type that has an associated constant of the right type, or it could be a namespace with a const or static of the right type. You can assume that it's an alias for the namespace of usable enum variants, but there's no guarantee of this. Additionally, the renaming import can be arbitrarily far away.

Add to this that IDE completions can transparently support completion at _::{|} or Enum::{|} even when Enum is not in scope, but will have no idea for E::{|} without a renaming import already in scope.

The reading is functionally equivalent between E::Yes and _::Yes (because in a reasonable codebase, you can indeed reasonably assume that it's an alias for the enum). The primary difference is in the authoring experience AIUI (and that may lessen the rationale for the feature).

This is because you need to know the name and path of the enum, and type it out in full at least once. For enums that are used only once in a file (which I argue could be common), it doesn't do anything helpful.

init(.Turbo, .Encabulator, .Prefabulated)

gives a much nicer and shorter API than:

use enums::TurboEnum as T;
use enums::EncabulatorEnum as E;
use enums::PrefabEnum as P;
init(T::Enable, E::Enable, P::Enable)

Name lookup based on context is as useful as having method names. It's much nicer to have iter.map(x) instead of use std::iter::Iterator as I; I::map(iter, x).

4 Likes

Well, that's debatable. Personally I find

init(TurboEnum::Enable, EncabulatorEnum::Enable, PrefabEnum::Enable)

to be quite reasonable and nice. Although I can't help but wonder why 3 supposedly very different and unrelated enums all have the same Enable, Disabled variants. In realistic APIs those enums would also be much more reasonable, and if they carry some data, then the enum name wouldn't be the biggest source of verbosity anyway.

Why does it matter? You need a value of type U, you get a value of type U. You can't supply a different value due to typechecking, and there may be many different ways to construct that value. Most of the time you wouldn't even pass raw enum variants around, you would have a variant returned by some function and bound to some variable, and you can name that variable whatever you like.

I also wouldn't want to make associated consts even more second-class to enum variants than they already are. The lack of variant aliases is very constraining in some context (e.g. if you're translating some C API, which has no qualms with several enum items with the same numeric value). At least currently we have a reasonable way to side-step it with associated consts (although they don't support all the ways a variant may be used). With special syntax for native variants, associated consts would stick out in code like a sore thumb.

I'm also super wary of the ways that such added implicit typing could interact with other implicit features of the language. E.g. like your example with Into<Enum>, how would all kinds of generic parameters interact with that feature? Could it cause some ambiguous interaction with trait resolution? _::Foo.bar() --- which bar method is called and where is it defined?

Hopefully, all such potential ambiguities would just result in a compile error, but at that point I expect half of the advantages of the short syntax to evaporate.

1 Like

These proposals all depend on inference, so that just wouldn't work, the same way that 1.0.tan() doesn't work and similar.

1 Like

What's certain is that we need something to disambiguate between such enum variants. That's why I've suggested that we could append enum name after variant name so there would be _YesAllowUnstable, _YesCapturing and so on. I don't think that we could invent any better disambiguator than an enum name.

The idea which @kornel suggested could be interesting:

I even thought previously about a similar alias but common for every enum variant, which was supposed to be appended as a suffix to a variant name e.g. #[alias = "Unstable"] enum AllowUnstable { Yes, No } then enables _::YesUnstable and _::NoUnstable.

But despite that looked simpler than AllowUnstable::Yes and AllowUnstable::No such feature had been still hard to justify: most likely in most of cases alias would be just a substring of enum's name — a few symbols shorter but producing a level of indirection without any significant gain.

That said, perhaps we don't need every enum variant to have its own level of indirection either.

Update on syntax

Recently I've revised the _YesAllowUnstable to make it have an explict boundary between enum variant and enum name. The idea has been that the _ (or whathever other inference symbol we choose) doesn't need to be in prefix position — it's possible to place it between enum variant and enum name.

The initial thought was to add something like Yes_AllowUnstable but this, unsurprisingly, was later dismissed.

So, next thought was to merge it with type ascription e.g. Yes: AllowUnstable and this turned to be promising. For example, this is how part of topic example looks with it:

let should_visit_node_again = match item.kind {
	// For use/extern crate items, skip rewriting attributes but check for a skip attribute.
	Use(..) | ExternCrate(_): ItemKind => {
		if contains_skip(attrs) {
			self.push_skipped_with_span(attrs.as_slice(), item.span(), item.span());
			false
		} else {
			true
		}
	}
	// Module is inline, in this case we treat it like any other item.
	_ if !is_mod_decl(item) => {
		if self.visit_attrs(&item.attrs, Outer: AttrStyle) {
			self.push_skipped_with_span(item.attrs.as_slice(), item.span(), item.span());
			false
		} else {
			true
		}
	}
	// Module is not inline, but should be skipped.
	Mod(..): ItemKind if contains_skip(&item.attrs) => false,
	// Module is not inline and should not be skipped. We want
	// to process only the attributes in the current file.
	Mod(..): ItemKind => {
		filtered_attrs = filter_inline_attrs(&item.attrs, item.span());
		// Assert because if we should skip it should be caught by
		// the above case.
		assert!(!self.visit_attrs(&filtered_attrs, Outer: AttrStyle));
		attrs = &filtered_attrs;
		true
	}
	_ => {
		if self.visit_attrs(&item.attrs, Outer: AttrStyle) {
			self.push_skipped_with_span(item.attrs.as_slice(), item.span(), item.span());
			false
		} else {
			true
		}
	}
};

Especially interesting is that we can "ascribe" multiple variants at once, since that seems to be quite viable alternative to proposals like path::Enum::{VariantA | VariantB}.

But still problematic is that we would utilize the X: Y syntax which might be also required for an actual type ascription or for named arguments or for anything else. Moreover, that syntax turned to look ambiguous in struct literals which is a really big deal. And I either didn't liked the space-gap between enum variant and enum name which prevents them being perceived as a whole.


Ultimately I've ended up with Variant.EnumName syntax. This may be either not perfect, although currently I'm convinced that it's the best solution that we can have.

Below is how the same example looks with it:

let should_visit_node_again = match item.kind {
	// For use/extern crate items, skip rewriting attributes but check for a skip attribute.
	Use(..) | ExternCrate(_).ItemKind => {
		if contains_skip(attrs) {
			self.push_skipped_with_span(attrs.as_slice(), item.span(), item.span());
			false
		} else {
			true
		}
	}
	// Module is inline, in this case we treat it like any other item.
	_ if !is_mod_decl(item) => {
		if self.visit_attrs(&item.attrs, Outer.AttrStyle) {
			self.push_skipped_with_span(item.attrs.as_slice(), item.span(), item.span());
			false
		} else {
			true
		}
	}
	// Module is not inline, but should be skipped.
	Mod(..).ItemKind if contains_skip(&item.attrs) => false,
	// Module is not inline and should not be skipped. We want
	// to process only the attributes in the current file.
	Mod(..).ItemKind => {
		filtered_attrs = filter_inline_attrs(&item.attrs, item.span());
		// Assert because if we should skip it should be caught by
		// the above case.
		assert!(!self.visit_attrs(&filtered_attrs, Outer.AttrStyle));
		attrs = &filtered_attrs;
		true
	}
	_ => {
		if self.visit_attrs(&item.attrs, Outer.AttrStyle) {
			self.push_skipped_with_span(item.attrs.as_slice(), item.span(), item.span());
			false
		} else {
			true
		}
	}
};

Vs original:

let should_visit_node_again = match item.kind {
	// For use/extern crate items, skip rewriting attributes but check for a skip attribute.
	ast::ItemKind::Use(..) | ast::ItemKind::ExternCrate(_) => {
		if contains_skip(attrs) {
			self.push_skipped_with_span(attrs.as_slice(), item.span(), item.span());
			false
		} else {
			true
		}
	}
	// Module is inline, in this case we treat it like any other item.
	_ if !is_mod_decl(item) => {
		if self.visit_attrs(&item.attrs, AttrStyle::Outer) {
			self.push_skipped_with_span(item.attrs.as_slice(), item.span(), item.span());
			false
		} else {
			true
		}
	}
	// Module is not inline, but should be skipped.
	ast::ItemKind::Mod(..) if contains_skip(&item.attrs) => false,
	// Module is not inline and should not be skipped. We want
	// to process only the attributes in the current file.
	ast::ItemKind::Mod(..) => {
		filtered_attrs = filter_inline_attrs(&item.attrs, item.span());
		// Assert because if we should skip it should be caught by
		// the above case.
		assert!(!self.visit_attrs(&filtered_attrs, AttrStyle::Outer));
		attrs = &filtered_attrs;
		true
	}
	_ => {
		if self.visit_attrs(&item.attrs, AttrStyle::Outer) {
			self.push_skipped_with_span(item.attrs.as_slice(), item.span(), item.span());
			false
		} else {
			true
		}
	}
};

Since in Rust things like CamelCase.CamelCase are very uncommon I don't think that it looks ambiguous with anything. But it either doesn't look "motley" amid other code (while :: in enum paths IMO looks). And because this syntax would rely on type inference I don't think that compiler would have a big trouble to deal with it.

Perhaps only the reversed Variant.EnumName order which in every other popular language always has been EnumName.Variant would be strange for some users until they see benefit.

What I value is the emphasis. For example when I see let allow_unstable = I expect that Yes or No would follow rather than stability::AllowUnstable or just the same AllowUnstable; but I either don't expect that Yes or No would come out of nowhere e.g. from glob-use in between of a bunch of similar statements at the very top of file. The let allow_unstable = Yes.AllowUnstable; seems to create the right emphasis.

To be fair, indeed something like let allow_unstable = _YesAllowUnstable; may create a wrong emphasis and it may look like I've just removed some symbols in an attempt to create "something" new.


Both variations may look like that. Unfortunately, sometimes only other people may point that.

I don't quite follow - what makes Outer.AttrStyle so much better than AttrStyle::Outer that it justifies the syntax churn?

2 Likes

Yeah, the point is to avoid writing enum's name at all in places where it's obvious from the context and would be needlessly repetitive. Instead of:

button.border_style(BorderStyle::Dotted);
button.border_style(Dotted.BorderStyle);

write:

button.border_style(_::Dotted);
button.border_style(.Dotted);
2 Likes

Personally, I don't see the value in that one. (And it's probably a breaking change, because let Variant = something_with_a_EnumName_field; is legal today. The prefix-dot form didn't have that problem.)

The big win for me of the general feature is that sometimes the type is contextually-obvious enough to not need to write it, but if it needs to be written out, then having it after instead of before seems not worth the effort, when EnumName::Variant could just be made to work without a use, like was previously mentioned:

2 Likes

In your proposal, when is it required to use Variant.EnumName instead of Variant? Your example's first pattern is Use(..) | ExternCrate(_).ItemKind.

If the answer is “always, that was a typo” — how is this meaningfully better (rather than just aesthetically) than Enum::Variant?

If the answer is “when {something else with the same name as the variant name} exists” — name in what scope? Local lookup — how is this better than Enum::Variant (as above)? Some larger scope — now new names outside the local scope impact the code's well-formedness?

...

Is the enum name required to be in scope? Either way — how is this better than Enum::Variant (as above)?

All paths, whether namespace::path or field.path go from general to specific. This would be the one only occurrence of “name (in context)” rather than “(in context) name”. This objectively makes the syntax a worse fit for Rust than just flipping the direction.

1 Like

It does exactly the same job as _::Outer but differently: it moves enum name into position where it's less intrusive rather than completely eliding it. In my opinion that difference is necessary in order to avoid the problem which @scottmcm mentioned:

That said, it's just an attempt to create a next iteration of _::Variant. I don't know whether in the end it would be possible to motivate such addition to the language itself. This is complicated task, at least for me.

How can you tell when the type is contextually obvious? And do we really have a lot of situations like that in Rust to claim that skipping writing types is a big win?


When there's no use EnumName::Variant or use EnumName::* at the top. In most of cases Variant.EnumName seems to be the right choice, and when we really though that EnumName is redundant in code we can import it into scope. IMO, logic is the same as when we decide whether to use EnumName::Variant instead of Variant but with Variant.EnumName the first choice could become more attractive.

This example shows that we can type enum name once per OR pattern in order to avoid repetitions. It seems to look better than something like Enum::{ VariantA | VariantB } and I think this makes it better than Enum::Variant.

Why does it matter? In most of cases I can read code with Variant.EnumName more effectively than code with EnumName::Variant. It has the same direction as value: Type, 0i32, and expr: Ascription, and I don't care that all paths are directed oppositely. Especially because the purpose of this syntax is to hide paths.

Well, the whole existence of inference is about not writing types, as is method syntax.

It's interesting that it's .map(|x| x.ok()), but .map(Result::ok). If _::Variant works, though, should .map(_::ok) work too? I don't know.

Here's a previous post with a bunch of examples of places where inference already allows APIs that can do things without saying the types involved:

But the situation that seems most clear to me is in struct literals.

Options {
    cookie_handling: CookieHandling::Skip,
    ..
}

That's pretty repetitive. Having just

```rust
Options {
    cookie_handling: .Skip,
    ..
}

seems quite reasonable to me.

2 Likes

Wait, that's what that was?

So with or patterns $pat | $pat, the meaning of one pattern depends on the other.

Yikes.