Rewritten proposal about `_::Variant`

TL;DR

Add _::identifier expression, resolving to constructors and constructed values(defined below).

Feature name

enhanced_constructors

Unnormative concepts(names subject to bikeshedding)

constructor: Enum variant with parenthesis; Associated functions returning Self. (All these are callable with Self type as return type)

constructed value:Enum variant without parenthesis and braces; Associated consts which has the Self type.(All these have Self as their type)

Motivation

This is syntactically similar to existing path syntax, providing an useful supplement to the currently unuseful <_>::identifier syntax, and the proposed behavior is similar to what people think it should behave(Intuitive). It can also shorten code, and reduce syntactical noise in many cases.

This is also making the existing constructor concept more useful, and make it inline with the method concept.

Reference level

Constructor definition

See the "Unnormative concepts"

Constructor lookup

Similar to method lookup, constructor lookup is a process that chooses a proper inferred constructor from candidates. The type and trait definitions must be in scope to participate. (Though for diagnostics purpose, more types could be involved)

When more than one candidate is applicable, ambiguous error is raised.

Notes

  1. Yes, this will make _::MAX, _::MIN, _::CapitalX, _::default(), _::new(), _::new(blah), _::new_in(foo, bar), _::from(), _::Ok, _::None, _::not(flag), … all valid rust (when the types are in scope of course), and has a large effect on changing the "casual Rust code". I don't think it's a bad thing, though.

  2. In the proposal, I intentionally left out dealing with struct definitions, simply because there's no corresponding associated item yet.

  3. If accepted, people might seek adding a subset of this to patterns in the future.

  4. @scottmcm has some notes in the past about this might change how people name enum variants. I don't think it will have much effect if people are mostly using IDEs that have inlay hints pointing out the inferred type (Rust-analyzer and other tools can supplement information only when the code didn't spell them out)

1 Like

There have been a lot of proposals for this. I like the broad scope of this one, though: you're also proposing to cover functions. I think we should try this as an experiment, and see how well it works.

Roughly speaking:

  • Find all the things in scope that have the specified name.
    • If there are none, error.
    • If there's exactly one, use that one.
    • If there's more than one, note the types of all of them, and use type inference to figure out which type would be acceptable.
      • If none would be, error.
      • If exactly one would be, use that one.
      • If more than one would be, error.

This is very different than the equivalent Swift feature, in that it is not described to depend on the use site context except as a filter. That is, with the Swift-style rule,

match value {
  _::Foo => {}
  _::Bar(baz) => {}
}

Foo and Bar will only be looked up in the type of value. As described by @josh, this would instead ignore the type of value and look in every name in scope before filtering down…which may not even include the type of value! I don’t think that’s useful or even reasonable behavior.

As an analogy, consider passing Default::default() to a function that takes a Foo, a type not in scope. Does the compiler try every type in scope that implements Default before giving up? Of course not. It infers the correct type from the call context and things work fine.

3 Likes

This feels like a semver hazard to me (though a soft one as you can always be explicit and support both before and after changes).

Hmm, this very much worries me, because it feels like the same kind of "oops trait resolution" problems we've been trying to avoid.

And of course, if it starts from

then you wouldn't need the _:: on the enum variant, right?

I don't know much Swift yet, but currently in rustc there's a gulf between the resolution step and type inference step. Rust has a bidirectional type inference that almost works in a single step (modulus things like fallback).

For your Default::default() example, Default is in effect and can direct the type inference, and it's different. _::foo is more "structural" in it is name directed instead of DefID directed, so for similarity it is more like method lookup (a.foo(bar)). Does the compiler try every type (Foo, &Foo, ...) in its candidate list when doing method lookup? Yes it does.

However it's maybe theorically possible to remediate the candidate list to include those types that are not yet in scope but raised from intermediate inference results. This detail might be find out in the implementation stage.

I interpreted josh's sentence as "Find all the types and traits in scope, and their constructors/constructed values that has the specific name."

And _:: is only useful when there's not a use Enum::Variant first.

See, personally that's not how I've been imagining this either, because then it still needs the use.

I've been thinking of this like a per-name version of Default::default(), where it doesn't care what's in-scope but does require that the type be unique from the inference context.

Like you imagine that .Foo translates as though there were a trait Foo { fn foo() -> Self; } and an impl Foo for MyEnum { fn foo() -> Self { Self::Foo } }, and you'd written <_ as Foo>::foo().

Because not having to care about the type -- don't think about it, don't put it in the use, etc -- seems like the point of this feature to me. Once you've used the type name, you could have just used the variants or given it a nice short name.

2 Likes

Default is a red herring here, if I define a free function fn my_default<T: Default>() -> T, I can use my_default() the same way I would use Default::default(), and it’s still going to infer the right T.

It’s true that this means you can’t do resolution ahead of inference, but as you say that’s no different from methods. If I write an expression foo().bar(), it’s no surprise that bar() depends on the return type of foo(), and can’t be looked up until we know it. The same is true of _::Bar { … }; it’s just that the “we know it” information comes from being an argument or pattern rather than being a return type.

(EDIT: Swift actually does add a structural constraint to its type inference, allowing more complicated and sometimes ill-advised expressions. But I don’t think that’s necessary for the feature.)

1 Like

I should have written that differently. "Find all the associated items with the specified name, among things in scope.".

Ok, so essentially the meaning that @crlf0710 guessed. I've responded to that one above.

I see your point. You're right, "in scope" is not quite the right constraint, because if the inferred type is unique, you shouldn't have to import it. For instance, func(_::Variant) should work if func's argument has a concrete enum type that has a variant Variant. (Likewise something like func(_::Variant(2, "s")).)

If we allowed only the inferred context, though, I think that doesn't give us enough information for the function case.

By way of example: we can always do takes_specific_type(_::new()) from the inference context alone, but could we allow let x = _::unique_func(value);, and should we? Maybe we don't want to allow that case, but that was what the algorithm I was writing was intended to allow.

If we don't want to allow that case, then I agree that we should just rely on the inferred type, and in that case, the only functions we should allow are those that return Self, such that when you write _::method(...), the return type is the same as the _ type.

As a test for this, how would we feel about this?

use std::num::NonZero;
let y: Option<_> = _::new(x);

I just get really worried about any time there's a "unique name in scope" that picks up things like inherent methods, since it could come from any of the imports, potentially.

Though there's something here that I do like, in that it might be nice for fallible constructors to be possible to pick up somehow. takes_file(_::create_new(p)?) doesn't seem implausible, but I'm not sure how best to write a rule to accept it that isn't also really scary.

2 Likes

That's extremely valid. I like the idea of takes_file(_::create_new(p)?), while your NonZero example seems really yikes in the absence of some additional thing later that involves y and constrains the type further to NonZero.

On balance, we're probably better off limiting how much inference we do here.

2 Likes

Not that I'm suggesting it's necessarily a desirable idea, but a potential different intuition for _::func(x), based off of how <_>::func(x) currently behaves, is that it would do the similar method lookup as x.func() except without the autoref step, only searching autoderef.

I think it'd be quite unfortunate if _::func(x) and <_>::func(x) would ever both compile but mean different things. Since _::constructor lookup is based on matching Self/output type and includes inherent items, whereas <_>::method lookup is based on receiver type and only looks at trait items, it's absolutely possible that the two could both resolve to separate items.

This could be remedied by making <_>:: and _:: both search the same candidates over an edition boundary — the fact <_>::method ignores inherent items is subtle and rarely actually relevant, since usually people write Trait::method when they want the trait method instead of a shadowing inherent method — but it bears keeping in mind, and would imply _:: also does receiver search, not constructor search.

3 Likes