Skip to content

Conversation

@JoshBashed
Copy link

@JoshBashed JoshBashed commented Jun 7, 2023

This RFC proposes a leading-dot syntax for path inference in type construction and pattern matching. When the expected type is known from context, developers can write .Variant, .Variant { … }, .Variant(…), .{ … }, or .(…) instead of typing the full type name.

struct Vector2 {
    x: isize,
    y: isize,
}

let vector: Vector2 = .{ x: 20, y: 20 };

fn print_result(result: Result<usize, String>) { /* ... */ }

print_result(.Ok(20));

Rendered

@JoshBashed JoshBashed changed the title Infered enums Infered types Jun 7, 2023
@Lokathor
Copy link
Contributor

Lokathor commented Jun 7, 2023

I'm not necessarily against the RFC, but the motivation and the RFC's change seem completely separate.

I don't understand how "people have to import too many things to make serious projects" leads to "and now _::new() can have a type inferred by the enclosing expression".

@ehuss ehuss added the T-lang Relevant to the language team, which will review and decide on the RFC. label Jun 7, 2023
@JoshBashed
Copy link
Author

I don't understand how "people have to import too many things to make serious projects" leads to "and now _::new() can have a type inferred by the enclosing expression".

In crates like windows-rs even in the examples, they import *. This doesn't seem like good practice and with this feature, I hope to avoid it.

use windows::{
    core::*, Data::Xml::Dom::*, Win32::Foundation::*, Win32::System::Threading::*,
    Win32::UI::WindowsAndMessaging::*,
};

@Lokathor
Copy link
Contributor

Lokathor commented Jun 7, 2023

Even assuming I agreed that's bad practice (which, I don't), it is not clear how that motivation has lead to this proposed change.

@JoshBashed
Copy link
Author

Even assuming I agreed that's bad practice (which, I don't), it is not clear how that motivation has lead to this proposed change.

How can I make this RFC more convincing? I am really new to this and seeing as you are a contributor I would like to ask for your help.

@Lokathor
Copy link
Contributor

Lokathor commented Jun 7, 2023

First, I'm not actually on any team officially, so please don't take my comments with too much weight.

That said:

  • the problem is that you don't like glob imports.
  • glob imports are usually done because listing every item individually is too big of a list, or is just annoying to do.
  • I would expect the solution to somehow be related to the import system. Instead you've expanded how inference works.

Here's my question: Is your thinking that an expansion of inference will let people import less types, and then that would cause them to use glob imports less?

Assuming yes, well this inference change wouldn't make me glob import less. I like the glob imports. I want to write it once and just "make the compiler stop bugging me" about something that frankly always feels unimportant. I know it's obviously not actually unimportant but it feels unimportant to stop and tell the compiler silly details over and over.

Even if the user doesn't have to import as many types they still have to import all the functions, so if we're assuming that "too many imports" is the problem and that reducing the number below some unknown threshold will make people not use glob imports, I'm not sure this change reduces the number of imports below that magic threshold. Because for me the threshold can be as low as two items. If I'm adding a second item from the same module and I think I might ever want a third from the same place I'll just make it a glob.

Is the problem with glob imports that they're not explicit enough about where things come from? Because if the type of _::new() is inferred, whatever the type of the _ is it still won't show up in the imports at the top of the file. So you still don't know specifically where it comes from, and now you don't even know the type's name so you can't search it in the generated rustdoc.

I hope this isn't too harsh all at once, and I think more inference might be good, but I'm just not clear what your line of reasoning is about how the problem leads to this specific solution.

@JoshBashed
Copy link
Author

Is your thinking that an expansion of inference will let people import less types, and then that would cause them to use glob imports less?

Part of it yes, but, I sometimes get really frustrated that I keep having to specify types and that simple things like match statements require me to sepcigy the type every single time.

whatever the type of the _ is it still won't show up in the imports at the top of the file. So you still don't know specifically where it comes from, and now you don't even know the type's name so you can't search it in the generated rustdoc.

Its imported in the background. Although we don't need the exact path, the compiler knows and it can be listed in the rust doc.

I hope this isn't too harsh all at once, and I think more inference might be good, but I'm just not clear what your line of reasoning is about how the problem leads to this specific solution.

Definitely not, you point out some great points and your constructive feedback is welcome.

@BoxyUwU
Copy link
Member

BoxyUwU commented Jun 7, 2023

Personally _::new() and _::Variant "look wrong" to me although i cant tell why, I would expect <_>::new() and <_>::Variant to be the syntax, no suggestion on _ { ... } for struct exprs which tbh also looks wrong but <_> { ... } isnt better and we dont even support <MyType> { field: expr }

@SOF3
Copy link

SOF3 commented Jun 12, 2023

I would like to suggest an alternative rigorous definition that satisfies the examples mentioned in the RFC (although not very intuitive imo):


When one of the following expression forms (set A) is encountered as the top-level expression in the following positions (set B), the _ token in the expression form should be treated as the type expected at the position.

Set A:

  • Path of the function call (e.g. _::function())
  • Path expression (e.g. _::EnumVariant)
  • When an expression in set A appears in a dot-call expression (expr.method())
  • When an expression in set A appears in a try expression (expr?)
  • When an expression in set A appears in an await expression (expr.await)

Set B:

  • A pattern, or a pattern option (delimited by |) in one of such patterns
  • A value in a function/method call argument list
  • The value of a field in a struct literal
  • The value of a value in an array/slice literal
  • An operand in a range literal (i.e. if an expression is known to be of type Range<T>, expr..expr can infer that both exprs are of type T)
  • The value used with return/break/yield

Set B only applies when the type of the expression at the position can be inferred without resolving the expression itself.


Note that this definition explicitly states that _ is the type expected at the position in set B, not the expression in set A. This means we don't try to infer from whether the result is actually feasible (e.g. if _::new() returns Result<MyStruct>, we still set _ as MyStruct and don't care whether new() actually returns MyStruct).

Set B does not involve macros. Whether this works for macros like vec![_::Expr] depends on the macro implementation and is not part of the spec (unless it is in the standard library).

Set A is a pretty arbitrary list for things that typically seem to want the expected type. We aren't really inferring anything in set A, just blind expansion based on the inference from set B. These lists will need to be constantly maintained and updated when new expression types/positions appear.

@JoshBashed
Copy link
Author

s (set A) is encountered as the top-level expression in the following positions (set B), the _ token in the expression form should be treated as the type expected at th

That is so useful! Let me fix it now.

@SOF3
Copy link

SOF3 commented Jun 12, 2023

One interesting quirk to think about (although unlikely):

fn foo<T: Default>(t: T) {}

foo(_::default())

should this be allowed? we are not dealing with type inference here, but more like "trait inference".

@JoshBashed
Copy link
Author

One interesting quirk to think about (although unlikely):

fn foo<T: Default>(t: T) {}

foo(_::default())

should this be allowed? we are not dealing with type inference here, but more like "trait inference".

I think you would have to specify the type arg on this one because Default is a trait and the type is not specific enough.

fn foo<T: Default>(t: T) {}

foo::<StructImplementingDefault>(_::default())

@SOF3
Copy link

SOF3 commented Jun 12, 2023

oh never mind, right, we don't really need to reference the trait directly either way.

@clarfonthey
Copy link

I've been putting off reading this RFC, and looking at the latest version, I can definitely feel like once the aesthetic arguments are put aside, the motivation isn't really there.

And honestly, it's a bit weird to me to realise how relatively okay I am with glob imports in Rust, considering how I often despise them in other languages like JavaScript. The main reason for this is that basically all of the tools in the Rust ecosystem directly interface with compiler internals one way or another, even if by reimplementing parts of the compiler in the case of rust-analyzer.

In the JS ecosystem, if you see a glob import, all hope is essentially lost. You can try and strip away all of the unreasonable ways of interfacing with names like eval but ultimately, unless you want to reimplement the module system yourself and do a lot of work, a person seeing a glob import knows as much as a machine reading it does. This isn't the case for Rust, and something like rust-analyzer will easily be able to tell what glob something is coming from.

So really, this is an aesthetic argument. And honestly… I don't think that importing everything by glob, or by name, is really that big a deal, especially with adequate tooling. Even renaming things.

Ultimately, I'm not super against this feature in principle. But I'm also not really sure if it's worth it. Rust's type inference is robust and I don't think it would run into technical issues, just… I don't really know if it's worth the effort.

@SOF3
Copy link

SOF3 commented Jun 12, 2023

@clarfonthey glob imports easily have name collision when using multiple globs in the same module. And it is really common with names like Context. Plus, libraries providing preludes do not necessarily have the awareness that adding to the prelude breaks BC.

@JoshBashed
Copy link
Author

And honestly, it's a bit weird to me to realise how relatively okay I am with glob imports in Rust, considering how I often despise them in other languages like JavaScript. The main reason for this is that basically all of the tools in the Rust ecosystem directly interface with compiler internals one way or another, even if by reimplementing parts of the compiler in the case of rust-analyzer.

In the JS ecosystem, if you see a glob import, all hope is essentially lost. You can try and strip away all of the unreasonable ways of interfacing with names like eval but ultimately, unless you want to reimplement the module system yourself and do a lot of work, a person seeing a glob import knows as much as a machine reading it does. This isn't the case for Rust, and something like rust-analyzer will easily be able to tell what glob something is coming from.

I can understand your point, but, when using large libraries in conjunction, like @SOF3 said, it can be easy to run into name collisions. I use actix and seaorm and they often have simular type names.

@JoshBashed
Copy link
Author

JoshBashed commented Jun 12, 2023

Personally _::new() and _::Variant "look wrong" to me although i cant tell why, I would expect <_>::new() and <_>::Variant to be the syntax, no suggestion on _ { ... } for struct exprs which tbh also looks wrong but <_> { ... } isnt better and we dont even support <MyType> { field: expr }

In my opinion, it's really annoying to type those set of keys. Using the QWERTY layout requires lots of hand movement. Additionally, it's syntax similar to what you mentioned has already been used to infer lifetimes, I am concerned people will confuse these.
Frame 1

@clarfonthey
Copy link

Right, I should probably clarify my position--

I think that not liking globs is valid, but I also think that using globs is more viable in Rust than in other languages. Meaning, it's both easier to use globs successfully, and also easier to just import everything you need successfully. Rebinding is a bit harder, but still doable.

Since seeing how useful rust-analyzer is for lots of tasks, I've personally found that the best flows for these kinds of things involve a combination of auto-import and auto-complete. So, like mentioned, _ is probably a lot harder to type than the first letter or two of your type name plus whatever your auto-completion binding is (usually tab, but for me it's Ctrl-A).

Even if you're specifically scoping various types to modules since they conflict, that's still just the first letter of the module, autocomplete, two colons, the first letter of the type, autocomplete. Which may be more to type than _, but accomplishes the goal you need to accomplish.

My main opinion here is that _ as a type inference keyword seems… suited to a very niche set of aesthetics that I'm not sure is worth catering to. You don't want to glob-import, you don't want to have to type as much, but also auto-completing must be either too-much or not available. It's even not about brevity in some cases: for example, you mention cases where you're creating a struct inside a function which already has to be annotated with the type of the struct, which cannot be inferred, and therefore you're only really saving typing it once.

Like, I'm not convinced that this can't be better solved by improving APIs. Like, for example, you mentioned that types commonly in preludes for different crates used together often share names. I think that this is bad API design, personally, but maybe I'm just not getting it.

@programmerjake
Copy link
Member

I do think inferred types are useful when matching for brevity's sake:
e.g. in a RV32I emulator:

#[derive(Copy, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)]
pub struct Reg(pub Option<NonZeroU8>);

#[derive(Debug)]
pub struct Regs {
    pub pc: u32,
    pub regs: [u32; 31],
}

impl Regs {
    pub fn reg(&self, reg: Reg) -> u32 {
        reg.0.map_or(0, |reg| self.regs[reg.get() - 1])
    }
    pub fn set_reg(&mut self, reg: Reg, value: u32) {
        if let Some(reg) = reg {
            self.regs[reg.get() - 1] = value;
        }
    }
}

#[derive(Debug)]
pub struct Memory {
    bytes: Box<[u8]>,
}

impl Memory {
    pub fn read_bytes<const N: usize>(&self, mut addr: u32) -> [u8; N] {
        let mut retval = [0u8; N];
        for v in &mut retval {
            *v = self.bytes[addr.try_into().unwrap()];
            addr = addr.wrapping_add(1);
        }
        retval
    }
    pub fn write_bytes<const N: usize>(&mut self, mut addr: u32, bytes: [u8; N]) {
        for v in bytes {
            self.bytes[addr.try_into().unwrap()] = v;
            addr = addr.wrapping_add(1);
        }
    }
}

pub fn run_one_insn(regs: &mut Regs, mem: &mut Memory) {
    let insn = Insn::decode(u32::from_le_bytes(mem.read_bytes(regs.pc))).unwrap();
    match insn {
        _::RType(_ { rd, rs1, rs2, rest: _::Add }) => {
            regs.set_reg(rd, regs.reg(rs1).wrapping_add(regs.reg(rs2)));
        }
        _::RType(_ { rd, rs1, rs2, rest: _::Sub }) => {
            regs.set_reg(rd, regs.reg(rs1).wrapping_sub(regs.reg(rs2)));
        }
        _::RType(_ { rd, rs1, rs2, rest: _::Sll }) => {
            regs.set_reg(rd, regs.reg(rs1).wrapping_shl(regs.reg(rs2)));
        }
        _::RType(_ { rd, rs1, rs2, rest: _::Slt }) => {
            regs.set_reg(rd, ((regs.reg(rs1) as i32) < regs.reg(rs2) as i32) as u32);
        }
        _::RType(_ { rd, rs1, rs2, rest: _::Sltu }) => {
            regs.set_reg(rd, (regs.reg(rs1) < regs.reg(rs2)) as u32);
        }
        // ...
        _::IType(_ { rd, rs1, imm, rest: _::Jalr }) => {
            let pc = regs.reg(rs1).wrapping_add(imm as u32) & !1;
            regs.set_reg(rd, regs.pc.wrapping_add(4));
            regs.pc = pc;
            return;
        }
        _::IType(_ { rd, rs1, imm, rest: _::Lb }) => {
            let [v] = mem.read_bytes(regs.reg(rs1).wrapping_add(imm as u32));
            regs.set_reg(rd, v as i8 as u32);
        }
        _::IType(_ { rd, rs1, imm, rest: _::Lh }) => {
            let v = mem.read_bytes(regs.reg(rs1).wrapping_add(imm as u32));
            regs.set_reg(rd, i16::from_le_bytes(v) as u32);
        }
        _::IType(_ { rd, rs1, imm, rest: _::Lw }) => {
            let v = mem.read_bytes(regs.reg(rs1).wrapping_add(imm as u32));
            regs.set_reg(rd, u32::from_le_bytes(v));
        }
        // ...
    }
    regs.pc = regs.pc.wrapping_add(4);
}

pub enum Insn {
    RType(RTypeInsn),
    IType(ITypeInsn),
    SType(STypeInsn),
    BType(BTypeInsn),
    UType(UTypeInsn),
    JType(JTypeInsn),
}

impl Insn {
    pub fn decode(v: u32) -> Option<Self> {
        // ...
    }
}

pub struct RTypeInsn {
    pub rd: Reg,
    pub rs1: Reg,
    pub rs2: Reg,
    pub rest: RTypeInsnRest,
}

pub enum RTypeInsnRest {
    Add,
    Sub,
    Sll,
    Slt,
    Sltu,
    Xor,
    Srl,
    Sra,
    Or,
    And,
}


pub struct ITypeInsn {
    pub rd: Reg,
    pub rs1: Reg,
    pub imm: i16,
    pub rest: ITypeInsnRest,
}

pub enum ITypeInsnRest {
    Jalr,
    Lb,
    Lh,
    Lw,
    Lbu,
    Lhu,
    Addi,
    Slti,
    Sltiu,
    Xori,
    Ori,
    Andi,
    Slli,
    Srli,
    Srai,
    Fence,
    FenceTso,
    Pause,
    Ecall,
    Ebreak,
}
// rest of enums ...

@Aloso
Copy link

Aloso commented Jun 12, 2023

I do like type inference for struct literals and enum variants.

However, type inference for associated functions doesn't make sense to me. Given this example:

fn expect_foo(_: Foo) {}
foo(_::bar());
  • According to this RFC, the _ should be resolved to Foo (the function argument's type), but this isn't always correct. I suspect that this behavior is often useful in practice, but there are cases where it will fail, and people may find this confusing. For example, Box::pin returns a Pin<Box<T>>, so _::pin(x) couldn't possibly be inferred correctly.

  • Even when Foo has a bar function that returns Foo, there could be another type that also has a matching bar function. Then _ would be inferred as Foo, even though it is actually ambiguous.

  • Another commenter suggested that we could allow method calls after the inferred type (e.g. _::new().expect("..."), or _::builder().arg(42).build()?). But this still wouldn't help in a lot of cases, because methods often return a different type than Self (in contrast to associated functions, where Self is indeed the most common return type).

    For example, the _ in _::new(s).canonicalize()? can't be inferred as Path, because Path::canonicalize returns Option<PathBuf>.

  • Another issue is that it doesn't support auto-deref (e.g. when a function expects a &str and we pass &_::new() 1, which should be resolved as &String::new(), but that may be ambiguous).

All in all, it feels like this would add a lot of complexity and make the language less consistent and harder to learn.

Footnotes

  1. I realize this is a contrived example

@Aloso
Copy link

Aloso commented Jun 12, 2023

Regarding structs and enums: The RFC didn't explicitly mention this, but I think that tuple structs, tuple enum variants, and unit structs should also be inferrable:

enum MyEnum {
    NormalVariant { a: i32 },
    TupleVariant(i32),
    UnitVariant,
}

struct NormalStruct { a: i32 }
struct TupleStruct(i32);
struct UnitStruct;

fn expect_enum(_: MyEnum) {}
fn expect_normal_struct(_: NormalStruct) {}
fn expect_tuple_struct(_: TupleStruct) {}
fn expect_unit_struct(_: UnitStruct) {}

expect_enum(_::NormalVariant { a: 42 });
expect_enum(_::TupleVariant(42));
expect_enum(_::UnitVariant);

expect_normal_struct(_ { a: 42 });
expect_tuple_struct(_(42));
expect_unit_struct(_);

@knickish
Copy link

I use a trait which contains a function that takes an argument of a type which is only accessed as an associated type, and being able to replace all of that with a _::Variant or similar would save so much visual clutter and typing. Should be easy to infer as the function only takes that specific type as an argument, just lots of typing (that doesn't autocomplete very well because angle brackets) to get to it.

@clarfonthey
Copy link

clarfonthey commented Jun 12, 2023

I do think inferred types are useful when matching for brevity's sake:

Just gonna break down my thought here when I read this:

  1. Oh, there are underscores. What should fill in the blank?
  2. It's returned by Insn::decode. What does that return?
  3. Oh, it's Insn. Cool.

I'm not sure what's gained by doing this instead of adding a single use Insn::*; statement before the match, or a use self::Insn::* statement across the board and just relying on that throughout a codebase. To me, you're both refusing to rely on full local inference and refusing to rely on full non-local inference, instead relying on this weird half version where you have to add a _:: but nothing else. What does this add, here?

@idanarye
Copy link

For other art on similar proposals on the enum variant shorthand, there is C# and Dart (both also landing on dot syntax, which makes swift, C#, dart, and zig). Not sure if its worth to add to the proposal doc, or at least look to see if there is anything that can be gleaned from them.

If the . suggestion originates from these languages, than I would count it as a point against it instead of in favor. The reason is that in all these languages . is the path separator and regular enum usage looks like Enum.Variant, so they removed the Enum and remained with .Variant. But in Rust, the path separator is :: and regular enum usage looks like Enum::Variant. When you remove the Enum part you get ::Variant - which we can't use because it already has a meaning in Rust - which makes .Variant completely alien.

@RoloEdits
Copy link

If the . suggestion originates from these languages, than I would count it as a point against it instead of in favor.

Just so there is no confusion, this is not my RFC. I provided other examples of languages adding similar shorthand. The RFC itself states that swift is the main inspiration. Didn't mean to add confusion here. Sorry.

And this is just my own interpretation, but I believe the idea is not to intentionally copy the exact implementation of other languages solely because they do it too, the original idea was to use _ which these languages don't use and tried to match the already existing _ inferred type syntax, but that for similar ability, some sigil is needed (for rusts own ambiguities).

For example, () and {} have meaning already. So something needs to indicate that this is different. I like . personally more than the original _.

If the main hangup is the chosen sigil, then we can try to brainstorm others (we already cannot use a :: prefix for reasons already stated).

The ! is now becoming the never type, but even then there could be issues with !(). Would need to look more into this to see if this would truly be an issue.

@ could be interesting, @{}, @(), @Variant, @Variant{ foo, bar }, but as this syntax could be used in pattern matching, and we already have foo @ Enum::Variant, which would make ambiguity.

# is used for attributes, and I think that would be more confusing to be used here, and syn, for example, might not handle inline attributes for fields correctly without updating to support. If chosen, this would have to be looked into.

I believe (I haven't kept up) ~ use for ~const was changed to [const], so there could be ~{}, ~(), and ~Variant?

When you remove the Enum part you get ::Variant - which we can't use because it already has a meaning in Rust - which makes .Variant completely alien.

This is exactly the reason . (or earlier _) was chosen. This is a new syntax to indicate something new. I wouldn't get to caught up on the idea that . means something in those languages and matches more for them and so chosen as such. In the C# example, they point out that an alternative was to have just Variant, but chose the . prefix for ambiguities (In their case to prevent Color Color situations). They didn't choose it explicitly because it matched when the name was taken away by default and left the . behind.

I mentioned those other languages to point out that the other language designers looked at . to be better than other candidates. And that we might not be completely insane to choose . as an option ourselves. Each language was/is looking to add some shorthand way to express something. I would say converges, not originates, is probably more apt. And convergence in itself shouldn't make this RFC have weaker standing IMO.

@idanarye
Copy link

This is exactly the reason . (or earlier _) was chosen. This is a new syntax to indicate something new.

It was already discussed multiple times here, but given the sheer amount of comments I'm not expecting anyone to read everything so it's worth bringing up again:

The point of _ is not that it's currently unused at this position. The point of _ is that it already has an established meaning in Rust1, and that meaning is exactly what we want here.

_ - when placed in a position where a type should be - means "the compiler already knows the exact type that should go here, it will not accept any other type, so just have it fill the type in". This is exactly what we want in this syntax. This is not a new meaning for _ - it's a meaning that _ already has in Rust.

Footnotes

  1. Two established meanings, actually - but it's other meaning ("black hole") is in different syntactical positions.

@idanarye
Copy link

The RFC itself states that swift is the main inspiration. Didn't mean to add confusion here.

Swift, too, uses the Enum.Variant syntax (technically Enum.variant?) so my point remains.

@RoloEdits
Copy link

RoloEdits commented Dec 17, 2025

I'm not sure if the reponse is for general clarity, or if I stated something that would suggest this and this had to be clarified. So apologizes if below doesn't need to be said.

From my response:

the original idea was to use _ which these languages don't use and tried to match the already existing _ inferred type syntax

i.e. the Vec<_> syntax, not the _ = Foo syntax or the _ => {} syntax.

I didn't mean to imply the use in the discard pattern. I meant that the . syntax was not initially chosen, and show it tried to use something most similar initially, in response to the inference that the dot was chosen solely because the other languages chose it. It was to break that thought using historical evidence that the suggestion was untrue.

The main issue with _ here is the ambiguity. _ has meaning in the language, but nothing stops you from having a variant foo and then having _foo as the short hand.

In pattern matching this can lead to:

enum Foo { bar, baz }

let foo = Foo::baz;

match foo {
  _baz => {},
  _bar => {},
}

Is this captured variable? Is it a shorthand qualifier for the variant? This is an ambiguity, and a pretty hard one. And I'm pretty sure is a breaking change regardless.

It makes sense to me that why although _ matches the closest with something existing in the language, an alternative was looked at instead. That it happens to be the ., and that there is convergence, again, I don't think should be itself a negative. Some sigil is needed for this to work. _ cannot work, even though its the most similar. And .., being most similar to another related thing, also doesn't really provide benefit over just . here. I did already mention an issue for . above as well, when used in struct update syntax, so its not like its perfect, which I also don't mean to imply.

@idanarye
Copy link

_Variant was never an option and never should be because it's a legal identifier on its own. The _ suggestion is _::Variant. Which is not "_:: before the variant name to distinguish it from a standalone identifier" - it's "_ where the type should be followed by the regular :: for accessing something inside a type".

And again - this is not a new meaning for _. This is a meaning _ already has in Rust. Using . would be a new meaning for ..

@RoloEdits
Copy link

Swift, too, uses the Enum.Variant syntax (technically Enum.variant?) so my point remains.

I couldn't find the initial proposal for the implicit member expression shorthand in swift, but from looking at the enum dot proposal, it seems the . was further enforced for more than because you access it via Enum.variant, they also seemed to have had some issues with switch case ambiguity, similar to the example I wrote above with _.

enum Coin {
    case heads, tails
    func printMe() {
        switch self {
        case heads: print("Heads")  // no leading dot
        case .tails: print("Tails") // leading dot
        }
        
        if self == heads {          // no leading dot
            print("This is a head")
        }
        
        if self == .tails {         // leading dot
            print("This is a tail")
        }
    }

    init() {
        let cointoss = arc4random_uniform(2) == 0
        self = cointoss ? .heads : tails // mix and match leading dots
    }
}

And again - this is not a new meaning for _. This is a meaning_ already has in Rust. Using . would be a new meaning for ..

Right, I used _variant and not _::variant because there is also {} and () and enum use in places that don't necessarily need disambiguation. And having different syntax for different places to mean the same thing seems much worse to me personally.

We could just make all syntax, regardless if disambiguation needs, use this: _::{} _::(). But I think this defeats the purpose of a shorthand a bit.

enum Foo {bar}

let foo: Foo = _::bar; // Already clear here

struct Point(u32, u32);

let point: Point = _::(0, 0);

 let connection = Connection::open(
    _::{
        timieout: 1000,
        retries: 10
    }
 );

If having a single . (or single anything) fixes the issues around ambiguity, and not have to use _:: everywhere when not needed, and it keeps syntax consistent when using it in different places, I think this would be worth it.

@idanarye
Copy link

idanarye commented Dec 17, 2025

We could just make all syntax, regardless if disambiguation needs, use this: _::{} _::().

No, because you don't do Foo::{} and Foo::() - you do Foo {} and Foo(). The _ replaces the type:

Inference syntax Gets inferred as
Vec::<_>::new() Vec::<Foo>::new()
_ { bar: 1 } Foo { bar: 1 }
_(1) Foo(1)
_::Bar Foo::Bar

In all these cases - including the first, which is an existing syntax and works exactly the same as the suggested ones after it are supposed to work - _ gets inferred as Foo and all the other tokens remain exactly the same. No need for different rules for different syntactical patterns - one rule to rule them all.

@RoloEdits
Copy link

I think support for _:: structs would still need to be handled as you can have:

use std::marker::PhantomData;

struct PhantomTuple<A, B>(A, PhantomData<B>);

fn main() {
    let _tuple = PhantomTuple::<char, f32>('Q', PhantomData);
}

Although a little absurd, you would need to handle some kind of:

// Could also have `_<>` though this is a change from the goal of
// using `_` purely for the type in the syntax, as if done that way,
// it would need the `::`.
let _tuple: PhantomTuple<_, _> = _::<char, f32>('Q', PhantomData); 

For comparison, with just a dot this would look like:

let _tuple: PhantomTuple<_, _> = .<char, f32>('Q', PhantomData);

This is partially (similar family of issues) addressed in the RFC in the unresolved section.

In general, I would say that _:: does not really fulfill the role of the shorthand by much. I think if this is chosen as the preferred syntax this RFC wont ever be accepted. It just wouldn't move the needle enough to justify it.

To me, this RFC has the most value in creation contexts, not in pattern matching contexts. It would most heavily impact enums if only creation is supported, but if just for creation where the type can be inferred, it might be fine to have some form of _Variant or .Variant { foo: "foo", bar: "bar" }. This opens up the largest benefits of the better ergonomics.

If done this way, then there could be a clear distinguishing between inferring and creating as well, to help decide if _ use is better than a new sigil that signifies creation, leaving _ for inferring.

For future work, there is the possibility of variant types (or maybe now this is incorporated in pattern types?), and the shorthand here could probably go along ways in the ergonomics of the use there. I'm not sure how far along things are in any direction, but its something we must keep in mind as not wanting to conflict and block from acceptance.

Overall, I would say I fall in the camp of wanting the idea, but would prefer a single sigil syntax over a mix, where although there are existing rules that this fits logically, just don't do enough in all contexts to justify the extra ceremony done just to keep up with the rules in all those contexts.

And as said before, potentially limiting this RFC to just creation, and forbid in patterns, might also make the most sense, as you can argue there should be more context needed in those spots to best serve its purpose.

This can prevent some obvious destructing examples though, like:

struct BarBaz {bar: i8, baz: bool}
// Or `_ {bar, baz }`
let .{ bar, baz } = get_bar_baz(); // returns `BarBaz`

or in functions:

// Or `_(params):`
async fn query(.(params): Query<HashMap<String, String>>) {}

But that might be the trade-off(or again, maybe even desired) to make.

@JoshBashed
Copy link
Author

@idanarye

In all these cases - including the first, which is an existing syntax and works exactly the same as the suggested ones after it are supposed to work - _ gets inferred as Foo and all the other tokens remain exactly the same. No need for different rules for different syntactical patterns - one rule to rule them all.

I actually agree that the "one rule to rule them all" story for ‎_ is very appealing, and ‎_ { … }, ‎_::Variant, etc line up nicely with how ‎Vec::<_> already works.

Once I started writing code with the leading dot syntax, it felt a lot nicer in practice than the underscore forms. It reminds me a bit of how the ‎? operator was received at first: many people felt it was “not Rust-like” until they had some real usage and it became part of the mental model of the language.

In Rust, a leading ‎. in this position currently has no meaning (99.99% of the time). In actual code, ‎.Variant, ‎.{ … }, etc ended up feeling more natural to me than the ‎_ variants, even if the ‎_ scheme is theoretically more "Rust-like".

As @RoloEdits, _ here would also get pretty awkward once you involve destructuring. (let _ { foo } = value;) It already has a meaning as a wild card or discard. I'd like to add on to this in match statements. It also might feel a bit awkward to use _::Variant because underscore is used near match and can also mean discard.

@RoloEdits

let tuple: PhantomTuple<, _> = _::<char, f32>('Q', PhantomData);

I'm actually not planning to support generics in inferred syntax.

@RoloEdits
Copy link

I'm actually not planning to support generics in inferred syntax.

That actually simplifies a lot!

I brought up the ...{} struct update syntax issue before, and although it doesn't make sense to actually use this here, some rule should be decided on to handle it. I for one think it should be outright disallowed.

It clears up the biggest mess of syntax, and leaves open the value spread/splat RFC. And its just not a pattern anyone should ever use. It should be fine in updated fields, as an example, but just not the trailing part, where it expects an existing Self.

Once I started writing code with the leading dot syntax, it felt a lot nicer in practice than the underscore forms. ... In actual code, ‎.Variant, ‎.{ … }, etc ended up feeling more natural to me than the ‎_ variants, even if the ‎_ scheme is theoretically more "Rust-like".

Having used this shorthand with a . in other languages, it definitely comes quick. This matches my own experiences with them. The main thing for me is the single sigil aspect, which _ cannot be in all places.

What is probably my most preferred sigil is actually @, but has the great problem with assignment on patterns, which would directly conflict with the foo @ Foo => pattern match.

After @, and taking into account using the same single sigil everywhere, I would order . and then~. ., as we know, exists in other languages to mean this exact shorthand. I think this is a plus! There is also the meaning of "this directory" when used in a cli. Kind of like "this is the thing" and operate on it. ~ has notion where its meaning is "approximate", similar enough in essence if you understood the use case: "The shape is enough, make the thing".

@programmerjake
Copy link
Member

As @RoloEdits, _ here would also get pretty awkward once you involve destructuring. (let _ { foo } = value;) It already has a meaning as a wild card or discard.

let _ { foo } = value; or let _(bar) = baz; all seem fine and unambiguous to me since the _ is where you'd put a type, not a pattern since stuff like let (A | B) { foo } = bar; is invalid syntax.

@idanarye
Copy link

Kind of like "this is the thing" and operate on it. ~ has notion where its meaning is "approximate", similar enough in essence if you understood the use case: "The shape is enough, make the thing".

What do you mean by "shape"? Structural typing? Because this should be type inference, not structural typing - the shape of type should not affect the inference.

@RoloEdits
Copy link

Ah, yeah, it wasn't meant to be taken exactly. I tried (and failed 😆 ) conveying a hand wavy nature, trying to hint at the compiler magically making it work. I know type inference will be what is used here to match the type, but just wanted to try to convey a sort of dumb concept around matching syntax to a "do the thing" thing, from its own perspective. A very dumb thing on my part, sorry for the confusion.

@tmccombs
Copy link

tmccombs commented Dec 19, 2025

In swift et al, "." is the namespace separator, so .Variant is just leaving off the name of the enum, but in Rust that would be ::Variant. But that won't work, because that already has a different meaning of using Variant from the root namespace.

I don't think using "." is terrible. I could certainly live with it. But it also feels a little out of place.

Would ":" be workable? :{ foo }, :(a, b), :Variant, etc. It is still a single sigil, but it is more similar to "::", and it is also more visible than ".". I'm not completely sold on that either though.

@kennytm
Copy link
Member

kennytm commented Dec 19, 2025

Would ":" be workable?

i think you need to demonstrate how this won't have the same negative effect to diagnostic that leads to #3307.

(actually the dot syntax may have the same issue, e.g. you'll accidentally invoke the feature in

let foo = Foo::builder();  // <- accidental `;`
    .config_1()  // <- 🤔
    .build();

)

@RoloEdits
Copy link

For destructing in a function (don't believe this is part of the actual proposal, but trying to be forward looking), it seems a little weird with the : flanking both sides.

fn foo(:( bar, baz ): BarBaz) {}
fn foo(:{ bar, baz }: BarBaz) {}

As for the unintentional invocation for ., I think with:

.config_1()

This would only be for a variant syntax, which would need something inside the (), as this would be the shape of the tuple enum variant (like .config_1(()) for example). I'm not clear how error/diagnostic reporting is implemented, or how easy this is to suggest, but there seems to exist a clear "I don't think this is what you mean" here, with a ; followed by an empty tuple enum (enum tuple?).

@idanarye
Copy link

This would only be for a variant syntax, which would need something inside the (), as this would be the shape of the tuple enum variant (like .config_1(()) for example).

Tuple variants can be empty in Rust: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=479522a0a68d14851244a2e0839d1e47

@tmccombs
Copy link

you need to demonstrate how this won't have the same negative effect to diagnostic that leads to #3307.

I don't think it would be quite as bad as type ascription, since, for example, missing a colon in the :: operator wouldn't be a valid place for path inference, since it can't come immediately after an identifier. But I'm not familiar enough with the diagnostic system to know how much (or how little) of a problem it would be.

@RoloEdits
Copy link

Tuple variants can be empty in Rust

Had never seen that before! Interesting. I wonder what you can do with this over just Variant? And while I don't think this will be a common pattern, it is another situation that can be tricky to provide helpful diagnostics for.

@steffahn
Copy link
Member

steffahn commented Dec 23, 2025

I would like to bring up the idea of ::-prefixes (without the _) again. I’m not convinced the counter-arguments here are necessarily valid.

Prior statements:


why I think in most case :: is enough, no needs the _::

it is ambiguous. Bare :: expressions are typically used to represent crates.

can you give me some example of that?

#3444 (comment)

I mean when you write this

match foo {
  ::foo => {}
}

This expression means "Look up a crate called foo regardless of current imports". It is a mechanism to allow proc macros to correctly reference a globally imported crate (e.g. ::std) even if the user manually imported something else as std.

While it is currently not possible to use a ::crate in any of the permitted positions for inferred types in this RFC, this may not be future-compatible (e.g. if we have Associated Traits #2190, we would have ::Foo::Bar which could mean both "Bar exported from the crate Foo" or "associated item Bar in the associated trait Foo of the inferred type") and just adds more complexity to the parser.


is it possible infer this way?

match dir as Direction {
     ::North => { .. }
     ::East => { .. }
     ::South => { .. }
     ::West => { .. }
}

#3444 (comment)

That would be ambiguous with other meanings of ::name, and it wouldn't make it obvious that something was omitted.


The only ambiguity mentioned here is a future-compatibility concern against what … ::Foo::Bar?? But this RFC isn’t about any “inferred type” syntax at all, anyway; it’s realy all about paths in patterns and (ordinary) expressions, isn’t it? In ::Foo::Bar, the left-hand side of the :: Bar is not an expression, it’s a type expression. You could also write ::Foo<Arg>::Bar there without a turbo-fish, and this all will be used in a place where types / trait bounds are expected…

And if ::Foo was an inferred path, from a :: version of this RFC (corresponding to .Foo in the current text) then it would instead be an enum’s variant / constructor. Those aren’t types; they are either syntactically parts of patterns, or – standalone – at best they can be values in expressions (e.g. unit-struct-style Enum variants as direct values; or tuple-style ones, which are technically functions). You cannot associate anything to a value in the first place!


IMO it’s quite the opposite of ambiguous (or confusing): Fully qualified paths are almost always longer than 1 segment anyways. When is the last time you would have used something silly such as the following?

use ::std as just_std;

Besides this niche case in use declarations, there’s not many more contexts (if any?) where ::crate_name can appear. From edition 2021 on, it’s only extern crates, basically.1 Almost all usages for this actually look like ::name::… instead. Additionally, extern crates are usually lowercase (snake_case), whereas constructors are uppercase (CamelCase) which makes it practically even less confusing.

Here’s some examples I had written, imagining in more variety of what simply using :: may look…

enum Demo {
	Variant1,
	Variant2,
	OtherVar,
	SomeTup(Foo, Bar),
	AndStructStyle {
		field1: i32,
		extra: Baz,
		moar_field: Qux,
	},
}

fn consume(d: Demo) {
	match d {
		::Variant1 => {
			// code
		}
		::Variant2, ::OtherVar => {
			// code 2
		}
		::SomeTup(foo, _) => {
			// some code
		}
		::AndStructStyle {
			field1: 123,
			extra,
			..
		} => {
			// handling
			// code
		}
		catch_all => {
			// keep calm and don't panic
		}
	}
}

fn consume_inline(d: Demo) {
	match d {
		::Variant1 => /* code */,
		::Variant2, ::OtherVar => /* code 2 */,
		::SomeTup(foo, _) => /* some code */,
		::AndStructStyle {
			field1: 123,
			extra,
			..
		} => {
			// handling
			// code
		}
		catch_all => /* keep calm and don't panic */,
	}
}

I think this RFC is most useful for enums, and could be quite beneficial even if initially only implemented / stabilized for enums alone. But in principle, structs can nonetheless be working in the same way:

struct Cool {
	field: Ty,
	other_field: Ty2,
}

struct LikeATuple(A, B, Cool);

fn need_tup_struct(t: LikeATuple) {
	let _field = t.2.field;
}

fn usage1() {
	// let's call
	let field = f();
	let c = :: {
		other_field: something(),
		field, 
	};
	// conventionally unsure about `:: {` vs `::{`
	// the former would be more consistent with the usual spacing
	// like  `Cool {`.
		
	// And we have tuple structs…
	let _ = need_tup_struct(::(foo1(), bar(), c));
	
	// Which can get ugly, I guess
	let _ = need_tup_struct(::(::(foo()), bar(), c));
	// but maybe let's just format these cases?
	let _ = need_tup_struct(
		::(
			::(foo()),
			bar(),
			c,
		),
	);
}
// some different example I found in the forum thread I’m just coming from
// [ https://users.rust-lang.org/t/path-inference-syntax-variant/136930/30 ]
// adapted to this syntax
fn test() {
	set_wireless_config(:: {
	    wlan: ::AccessPoint,
	    bluetooth: ::Enabled,
	});
}

though this does involve :: as “essentially” a stand-alone expression (interpreting :: as a function in the ::(…) expressions above.

It could even be fully stand-alone for unit structs, I guess?

// Unit structs?
struct Unity;

fn need_unity(_: Unity) {
	// wow
}

fn usage2() {
	// let's call it
	need_unity(::);
	// is this ^^ too cursed??
}

fn wow() {
	// if you are more used to “naked” `::`, then spacing
	// like this vvvv doesn’t look so unnatural anymore.
	let _value = :: {
		/* … */
	};
	
	// but IMHO this isn’t so unattractive either 🤷‍♂
	let _value = ::{
		/* … */
	};
}

The exact technical solution for distinguishing ::crate_name from ::Constructor for the compiler is probably not too important (given the little possibility for actual confusion for users), and hopefully not too complex. If ::crate_name (without immediately-following ::) is really only possible in use declarations, and I’m not missing any “obvious2 it should almost be trivial.3

Footnotes

  1. I will ignore edition ≤2018, as there is no actual need for this feature to be supported there. Or if there is, it can be solved by alternative syntax – maybe even allow _::, too, as an alternative after all? Or extend the meaning of <_>:: to include this case?

  2. they will always be obvious in hindsight – I’m looking forward to any replies that would point such a thing out

  3. If the extension to structs is included, the only real syntactic change is that :: can stand alone as an path now. And use declarations, which already have a special syntax tree anyway, would then be the only places where ::crate_name without subsequent :: would be interpreted the way it currently is.

    I suppose, we do also gain ambiguity for :: < … starting an expression – similar to turbo-fish. But it’s easily resolved (just disambiguate against the interpretation of :: (inferred unit struct constructor) “less than” …something, in favor or the start of an ::<TypeExpr…>::more_path…-kind of expression, and make the user write (::) < … if they for whatever reason really wanted the other thing (which probably is bound to fail inference of under-specified generics anyway?)

    If there’s any Option::Some::<u64>(123)-style syntax equivalent, and we allow ::Some::<u64>(123), then everything can still work, but now the criterion is no longer exactly differentiating as follows

    • ::ident::… [extern crate’s item path] vs ::ident … (with something being some tokens not starting in ::) [inferred constructor path]; and (as an extension also) standalone :: (not followed by <) [inferred unit-style constructor path].

    but AFAICT really, all that changes then after all is: the “not starting in ::” becomes “not starting in ::<”.

    Another area I have not thought too deeply about is macros though; I don’t recall off the top of my head, what macros could do w.r.t. parsing - and piecing together -paths.


Status::Pending<f64, Foo>(0.0);
Status::Complete<f64, Foo> { data: Foo::default() };
```
Copy link
Member

@steffahn steffahn Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn’t the correct current syntax. It would need to be

Status::Pending::<f64, Foo>(0.0);
Status::Complete::<f64, Foo> { data: Foo::default() };

with additional :: tokens after Pending/Complete, before the < tokens.

The section that follows then doesn’t correctly build analogy, either, of course. Maybe the “analogous” form then would be something like the following?

.Pending::<f64, Foo>(0.0);
.Complete::<f64, Foo> { data: Foo::default() };

For better understanding / completeness, in the former section, it might also be worth mentioning somehow that Status::Pending::<f64, Foo>(0.0) can also be written as Status::<f64, Foo>::Pending(0.0) in current Rust. So the former is just sugar; though for the case of .Pending, there wouldn’t really be any way to put the parameters before the variant name1, (which is a similar argument as for already the case for enum constructors that are use’d which – the need for Pending::<f64, Foo>(0.0) with use Status::Pending; – presumably motivated the existence of Status::Pending::<f64, Foo>(0.0) syntax in the first place.)

Footnotes

  1. Nevermind, maybe it’s not actually impossible. I just read the rest of the RFC and noticed that that’s even a mentioned possible / considered alternative [“.<u8, String>::Ok(42)”], and I see no immediate reason why this would be impossible.

@idanarye
Copy link

_ (for the type itself - with :: as path separator) is a candidate because it matches the way type inference already works in Rust, and fits in perfectly with the existing syntax rules (the only required change is to allow paths to start with a type inference marker)

. is a candidate because it looks nice. I'm in camp _, but I'm willing to admit that . is more visually appealing. It'll require more changes to the existing rules (in _::Foo { bar: baz } only the _ part is new - in .Foo { bar: baz } the entire thing (or at least the entire .Foo) is a new syntactic construct), and needs some reasoning regarding why it's not ambiguous, but it does look nice. And Rust is not exactly famous for its nice-looking syntax - so there is some merit in increasing the overall prettiness of it.

:: is dead last in both criteria.

  • Regarding fitting with existing syntax rules - it's worse than . because for both expressions and patterns it's illegal to start with . and legal to start with ::. While @steffahn's analysis shows it's still not ambiguous - it's a more convoluted argument and less future-proof and for that I place :: lower than . in this criteria.
  • Syntax elegance is highly subjective, so this bullet is not as strong as the previous one, but I think we can all agree that .Foo { bar: baz } looks nicer than either ::Foo { bar: baz } or _::Foo { bar: baz }. _::Foo { bar: baz } sticks out more than ::Foo { bar: baz }, but I don't think it's such a big difference. Of course, if that was all then just a difference would still be enough to give :: the second place - but when it comes to non-enums we get :: { foo: bar } which is absolutely atrocious, completely out-uglying whatever beauty points ::Foo { bar: baz } had over _::Foo { bar: baz }.

So even if :: is technically possible - why? If you want to prioritize pretty syntax, go with .. If you want to prioritize simpler syntax rules, go with _. If you don't want to prioritize either over the other - flip a coin between . and _, and whatever you get would still be better than :: in both criteria.

@programmerjake
Copy link
Member

programmerjake commented Dec 23, 2025

one issue with ::Foo::<'a> syntax is that could be ambiguous with crate-level generics, crate-level generics may be useful for things like:

  • enabling safely unloading dynamic libraries by changing everything provided by that crate (and its dependencies) to have a lifetime bound shorter than 'static.
  • maybe enabling platform-specific trait impls with things like changing std to be potentially:
    crate std<Platform: PlatformTrait = AnyPlatform>;
    impl From<u32> for usize
    where
        Platform: PointerIsAtLeast32bits,
    {
        ...
    }
    used like so:
    // opt-in to impls that assume `usize::BITS >= 32`, errors on 16-bit platforms
    extern crate std<impl PointerIsAtLeast32bits>;
    fn main() {
        let v: usize = 1234u32.into(); // works now
    }
  • probably other things?

@steffahn
Copy link
Member

steffahn commented Dec 23, 2025

  • Regarding fitting with existing syntax rules - it's worse than . because for both expressions and patterns it's illegal to start with . and legal to start with ::.

How is that an argument for why it’s worse though? Wouldn’t it already being legal mean less possibility for problems, because it’s already proven syntax? This should help not only with avoiding technical challenges, but also human ones; after all, an existing Rust user will have a much easier time understanding that ::Foo is a path (because it technically already is a path anyway) rather than learning the new concept of .Foo. I sure can remember, after being good at Rust (and also Haskell) both of which have algebraic data types, being really confused the first time I saw the .Foo kind of syntax in another language (I believe Zig) for the first time.

From that angle, I don’t really see the “beauty” in .Foo at all.

Also, these languages that do use .Foo as a shorthand for Bar.Foo, leaving off the type… oh wait, I see you have in fact just made this argument yourself a few day ago here :-) – otherwise I would have basically explained the very same point now!

For other art on similar proposals on the enum variant shorthand, there is C# and Dart (both also landing on dot syntax, which makes swift, C#, dart, and zig). Not sure if its worth to add to the proposal doc, or at least look to see if there is anything that can be gleaned from them.

If the . suggestion originates from these languages, than I would count it as a point against it instead of in favor. The reason is that in all these languages . is the path separator and regular enum usage looks like Enum.Variant, so they removed the Enum and remained with .Variant. But in Rust, the path separator is :: and regular enum usage looks like Enum::Variant. When you remove the Enum part you get ::Variant - which we can't use because it already has a meaning in Rust - which makes .Variant completely alien.

Of course don’t agree with the last sentence, ::Variant does not already have a meaning in Rust.


  • While @steffahn's analysis shows it's still not ambiguous - it's a more convoluted argument and less future-proof and for that I place :: lower than . in this criteria.

I don’t understand the future compatibiltiy concern. Is this just abstract raising of fear/uncertainty/doubts or are there actual concerns? My whole prior reply was based on my dissatisfaction with the ambigity-related counter-arguments, and I even explicitly noted in a footnote that “I’m looking forward to any replies that would point”.

IMO, the argument isn’t convoluted, it’s strong. It’s only lengthy because there are so many separate, independend ways in which (especially for human readers in pracrical programs) it’s an absolute non-issue.

First: Each one of these on its own could suffice to avoid all practical issues. Conventionally, crate names are lower-case, variant names upper-case.

(We already use the same distinction - for practical purposes - to differentiate between variants and variables in patterns;1 and in fact, this very RFC may drastically improve the situation there as people can just write ::Variant instead of Variant in most cases then.)

Second: The length difference… ::IDENT vs. ::IDENT::IDENT2 – this is the argument I have explained in most detail so far (because it’s making for the best arguments for why we don’t even need any disambiguation rule at all – unlike for example the rule for variables vs. struct/enum constructurs & const items, in patterns.

Third: I don’t know why I haven’t mentioned this yet – nobody even uses ::crate_name::path… syntax! Right? Who uses this syntax? I personally only know it because it’s useful for being defensive syntactically when writing a macro. I’m fairly certain, a significant portion of Rust users has never really (or at least barely) seen any path beginning with ::
before. You usually don’t write ::std::mem::drop(…foo…) but std::mem::drop(…foo…), right? I believe ::…-prefixed paths had more of an applicable use-case in the past, before the edition >2018 changes, but that was “removed” with the edition.

(It’s probably a reasonable thing to look into as to why exactly that was changed though and if it constitutes any new counter-arguments… I haven’t done this in-depth yet.)


  • Of course, if that was all then just a difference would still be enough to give :: the second place - but when it comes to non-enums we get :: { foo: bar } which is absolutely atrocious, completely out-uglying whatever beauty points ::Foo { bar: baz } had over _::Foo { bar: baz }.

I think this is a fair point. I would like to note that I personally feel – about this point – that the problem for structs is a lot less pressing anyway, and this RFC may potentially benefit from downsizing to fully nailing down the case of enum syntax only, as a first step.

Having given it a second thought now, I do feel like using ::Foo for enums, and _ for structs may even be a valid combination in and by itself. The use of _ can have the added benefic that _ can reasonably be permitted for (non-non_exhaustive) single-variant enums, too [those are a lot like structs anyway], and also _ for unit structs already matches the existing pattern we’d use for them 😁

struct Unit;
fn usage() {
    let x: Unit = _;

    match x {
        _ => { /* nothing new here! */ }
    }
}

whereas I hadn’t really considered the arguable weirdness of :: as a standalone pattern o.O

struct Unit;
fn usage() {
    let x: Unit = ::; // <- not really satisfying

    match x {
        :: => { /* I don’t really like it myself :-/ */ }
    }
}

_ (for the type itself - with :: as path separator) is a candidate because it matches the way type inference already works in Rust

As an added bonus, with ::Variant and _ alone, we’d be introducing a minimal amound of new syntax – both things are already technically valid expressions (syntactically) today – even though _ will currently only appear in destructuring assignment expressions… ah and also array lengths.


Regarding the argument of “_ means inference”, I’d like to propose that it’s more complex; and “leaving stuff out completely” is also something that already means inference!

That’s particularly true for constructors of structs/enums, where you can rewrite, for instance, Foo::Bar::<Arg> { field: 123, baz: baz } not only into Foo::Bar::<_> { field: 123, baz: baz } when Arg can be inferred, but also into the shorter alternative of Foo::Bar { field: 123, baz: baz } already. (By the way technically Foo::Bar::<> { field: 123, baz: baz } also appears to be legal.) Incidentally, you can also re-write this into Foo::Bar { field: 123, baz }. And you can re-write a type like &'a Qux not only into &'_ Qux (when elision - or inference ~ depending on the context, is applicable) but also into &Qux. There isn’t that much innovation hence in allowing for Foo::Bar { field: 123, baz } to be further shortened to ::Bar { field: 123, baz }, it’s just “leaving out some part of the syntax”. Incidentally, this kind of approach is also really nicely compatible with IDE inlay hints which may – for users that prefer to see more information – simply add back in the Foo prefix. The same thing doesn’t work quite that nicely for .Bar I suppose. [The next best thing they could aim for would be either a Foo.Bar / Foo.Bar kind of optic – or Foo::.Bar/Foo::.Bar.]

[On the other hand, another issue with the struct examples I provided earlier is that those do not constitute any shortening. Replacing Abc { arg: 123 } with :: { arg: 123 } would invent the :: out of thin air. I think I’m increasingly convinced of _ { arg: 123 } instead; I’m mainly mentioning :: only because it leaves open a possible alternative there, anyway.]


In any case, I belive adding ::Foo syntax as a possible alternative to this RFC text seems very necessary (not least given the amount of times it has been brought up).

Footnotes

  1. The technical mechanism on the other hand is terribly bad, basically “if it’s in scope as a constructor or constant, then it’s that, otherwise a variable” – is a rule you cannot even apply in your head if you wanted to, without knowledge of literally everything that’s in scope.

@steffahn
Copy link
Member

steffahn commented Dec 23, 2025

@programmerjake

one issue with ::Foo::<'a> syntax is that could be ambiguous with crate-level generics

That’s a good valid point to bring up, thank you! Here would be around 7 possibly counter arguments, anyways 🦀:1

  1. It’s fairly deep into speculative future-possibilities land.
  2. Depending on the scope of the feature, a possible ::crate_name::<'a> syntax might be mainly about lifetimes, whereas disambuguation for enum variants is generally only practically relevant for type arguments like ::Foo::<Ty>.
  3. It’s still generally a clear distinction between uppercase and lowercase. ::foo::<…> vs ::Foo::<…>.
  4. It’s still the case that most use-cases of crate names would not actually write that initial ::, so instead we’d have foo::<…> vs. ::Foo::<…>.
  5. If the argument was made that a plain-identifier crate name will never appear on its own (without another subsequent ::) unless in a use statement, wouldn’t this also apply to “crate name with generic argument”? So then it’s still ::foo::<Arg>::Item or ::foo::<Arg>::item::… [or something like use ::foo::<Arg> as xyz;] on the one hand; vs. ::Foo::<Arg> (followed by something other than ::) on the other hand.
  6. I’d be completely fine anyway with leaving ::Foo::<Arg>-style syntax for enum arguments as a future possibility, not part of the minimal version of the feature to be shipped.
  7. Even if ambiguities do occur, that’s not a hard technical issue. We can still just disambiguate (presumably then prioritizing the extern crates, if they exist) in much the same way as is the case for variables vs variants&constants in patterns. [Whereas on a human level, upper- vs. lower-casing, context, and the rarity of prefix-::-on-crates, still avoids practical problems.]

Footnotes

  1. I’m sorry if this seems excessive; I started writing the comment with “just” 3, but kept coming up with more 🙈

@RoloEdits
Copy link

RoloEdits commented Dec 24, 2025

But this RFC isn’t about any “inferred type” syntax at all, anyway; it’s realy all about paths in patterns and (ordinary) expressions, isn’t it?

I know the RFC says "Path reference", but I think this really only true if you take into account the "future work" section, like the let foo: Foo = .bar() syntax.

What the bulk of the discussion is around is basically just a specialization(shorthand) around type inference. And then from there the talk is about what best way to represent this in syntax. Reusing existing rules, with _, where that means type inference? Or going further in on the specialization case, and adding a new syntax, like ., to represent this specialness?

Also, these languages that do use .Foo as a shorthand for Bar.Foo, leaving off the type…

I mentioned before, at least from what I could find in the C# case, the choice of . was not just defaulted to because the path of Foo.Bar. And neither was this the default, first choice for this RFC. I actually couldnt find any proposal/rfc in those languages(zig and swift) that claimed this was why this syntax was chosen specifically; ., on its own, is not an insane choice for shorthand. Other languages have a .. to "spread" a value and create a new one. Rust picked this syntax as well.

I think we can acknowledge that other languages use it, and have similar goals for their proposals, providing shorthand, but beyond convergence, I think its not really productive to latch on to what other languages did, and especially to try to use that as a negative for what rust wants to do with its own syntax. We just need to find what works best.

As for ::, I am still very much in the camp of a single sigil. _, as mentioned, has issues there, and you end up having to use _:: for variants. I would rather a single way to represent this "specialized" type(variant) inference.

If variant types ever become a thing, I also would rather a .Variant shorthand than a _::Variant shorthand. Using ::Variant, for example in a generic function, generic over the variant, would be:

enum Foo { Bar, Baz }
fn foo<F: Foo>() { ... } 
let foo = foo<::Bar>();
// or
let foo = foo<_::Bar>();
// vs 
let foo = foo<.Bar>();

I know this is not assured to be a thing, but I think we should try to be broad minded were possible. With these simple enums, for example, this could also potentially be done with const generics, if it got expanded to them, which could have a higher likelihood of being done. But regardless, whether as a generic type or as a generic value, this example demonstrates the "specialized type inference" aspect I think this current RFC is really about.

I don't mean to hijack what this RFC is for with my own interpretation, but wanted to at least give my point of view on it. I think "Path inference", for the real guts of what this is introducing, leaving alone future work, is a bad name.

I think this is a fair point. I would like to note that I personally feel – about this point – that the problem for structs is a lot less pressing anyway, and this RFC may potentially benefit from downsizing to fully nailing down the case of enum syntax only, as a first step.

Interesting, as I had thought the opposite. With structs, specifically {} structs, it has the potential to actually provide a nice alternative to the builder pattern. You get the benefit of "named parameters" without adding, by itself, the downsides.

let connection = Connection::open(.{
    host: "localhost",
    port: 5672,
    username: "user",
    password: "bitnami",
})
.await
.unwrap();

This configuration pattern is used now, but its much more verbose, and often to provide a "nice" experience, the use of some Config struct has a new for which takes basic things, and anything thing else is left to other means to add. Sometimes even via a builder itself!

With the proposed and accepted default fields syntax, you could have the Config struct and use .. for the rest you can have be defaults.

let connection = Connection::open(.{
    host: "localhost",
    port: 5672,
    username: "user",
    password: "bitnami",
    ..
})
.await
.unwrap();

If there was a "partial default" support, where some fields become required (borrowing a c# implementation here, but rather than using positive space like adding a keyword we could use negative space), then this would add more versatility to the constraints for this config pattern (rather than needing to provide a function to enforce these required fields).

In the above example, host, port, etc. would not be default, and would therefore be required to provide (by means of its negative space of not being default). And for the rest, if you don't need anything special, just becomes ... Again, this uses more hypothetical concepts, but with just a few concepts and you get a much better way to express what you really want.

So in my mind, for both short term, having "named parameters" + the interaction with default fields, and long term, adding support for partial defaults in objects, adds more value overall, especially in user facing code, than what enums would get from this.

Though maybe I am just missing something obvious here and am not accounting for enum shorthand enough, or what other variant type/const proposals could bring that would promote the use of the shorthand even more.

In all, I am happy this RFC is getting a good amount of discussion, as this is something I think could really promote different design patterns and would love to see this added to the language.

@RoloEdits
Copy link

RoloEdits commented Dec 24, 2025

Also forgot to mention, with the {} config pattern, you not only get the named parameters, as mentioned, but with partial defaults, if they were to become a thing, you would effectively get function overloading, but done in a sane way. The required fields would not implement a default, and thus force to provide, and the rest of the fields just act as overloaded function variants.

(this would be different from zig, for example, as all values there have defaults by default, so nothing is required(compile error) to provide (.{} is valid, but could be wrong!). Partial defaults would act as a true overloaded function with required fields)

It brings in other language concepts, but done in a more sane way (at least to me), and this could honestly be my summary of rust as a whole.

This is more speculation(apologies), but I think a holistic approach is good here, as you can see the sum being greater than the parts.

(this is all to say that I dont think structs should be left out of this RFC)

@idanarye
Copy link

I am still very much in the camp of a single sigil

Why? I get the appeal of a single sigil, but single sigils are a limited resource - why is it so important for this particular feature to get one?

I think this is a fair point. I would like to note that I personally feel – about this point – that the problem for structs is a lot less pressing anyway, and this RFC may potentially benefit from downsizing to fully nailing down the case of enum syntax only, as a first step.

Interesting, as I had thought the opposite.

Regardless of what gets implemented first - assuming anything here gets implemented at all - I think we there is still merit in not compromising the design of the low-priority items without a good reasons.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-lang Relevant to the language team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.