Path inference syntax (.Variant, .{ … })

For some time now, I have wanted path inference in Rust to reduce repetition when constructing enums and structs. As such, RFC 3444 proposes leading-dot syntax for this. This allows you to write the following:

set_wireless_config(.{
    wlan: .AccessPoint,
    bluetooth: .Enabled
})

I implemented a prototype of this in the Rust compiler and I'm now looking for feedback on how it feels to actually use.

Links

Questions

  • Would you use this feature when writing Rust?
  • Does writing using this syntax feel natural?
  • If you were code reviewing, would this syntax harm your understanding of the code significantly?

Try It

Build the compiler:

git clone -b 3444 https://github.com/JoshBashed/rust.git
cd rust
./x.py build library

Create a test.rs:

#[derive(Debug)]
enum Fruit {
    Apple,
    Banana,
    Blueberry,
    Grape,
}

impl Fruit {
    fn color(&self) -> &'static str {
        match self {
            .Apple => "green",
            .Banana => "yellow",
            .Blueberry => "blue",
            .Grape => "purple",
        }
    }
}

fn main() {
    let fruit: Fruit = .Apple;
    println!("{:?} is {}", fruit, fruit.color());
}

Build and run:

build/<your-platform-triple>/stage1/bin/rustc test.rs
./test
6 Likes

I was just thinking to myself this morning how much I love that Zig lets me refer to things in such a short way, and how sad it is that Rust can't do that.

To be a bit more clear: yes, I would use and love the hell out of this feature. Thanks for your work!

3 Likes

Not keen on the idea. Seems to obfuscate code for the sake of saving a few characters. Hardly worth introducing more syntax for. I'm generally opposed to such "compression". Also seeing "." at the beginning of anything grates on my mind.

14 Likes

This is discussed every once in a while. It pertains to the more general question of whether it should be possible to elide and infer types in more contexts, for example with the _ wildcard . The Rusty version of .Foo would be _::Foo but the the general opinion seem to be that it looks ugly as hell. Similarly, .{} would probably become _ {} in Rust, which at least looks slightly less hideous.

4 Likes

I really don't want to see Rust look like morse code or line noise more than it does already. Especially not if only for the sake of brevity.

8 Likes

Originally, that was the proposed syntax. Someone from the team said that this syntax will be better. Because of that I switched to syntax.

After using it for a while, it does feel quite natural.

1 Like

May I ask what syntax you’re talking about? Is it the underscore version or the dot version?

Neither in particular. It's just my plea that in general we do not pile on more and more inscrutable syntax of odd uses of odd symbols to the language just for the sake of brevity.

4 Likes

Can you clarify what you find inscrutable about it?

Is the concern primarily about adding more syntax to the language, or about how the syntax reads in practice?

  1. No. I just import the enum into namespace.
  2. Not really.
  3. It would because as you mentioned in PR what happens if its
match self {
  (.Disabled, .Disabled) => {},
  (.Enabled, .Auto) => {},
  (_, .Enabled) => {},
}

It adds type confusion on top of this


Main gripe

My biggest worry is that this feature seems not to take Rust idioms into consideration. What about generic paths? What about min specialization and this feature working together?

#![feature(min_specialization)]

trait Trait {
    fn call();
}

enum MyOpt<T> {
   Ok(T),
   None,
   Err(String), 
}

impl Trait for Option<u32> {
    fn call() {
        println!("Opt<u32>")
    }
}
impl<T> Trait for MyOpt<T> {
    default fn call() {
        println!("MyOpt<T>")
    }
}

impl Trait for MyOpt<u32> {
    fn call() {
        println!("MyOpt<u32>")
    }
}


fn foo<T: Trait>(polymorphic: T) {
    T::call()
}

fn main(){
      foo(MyOpt::<u32>::None) 
      // foo(.None)   // WHAT IS BEING CALLED HERE??
}

Final note

More of a question—why even have the dot, if the goal is to be minimal?

Why even have the dot, if the goal is to be minimal?

Example 1

impl Fruit {
    fn color(&self) -> &'static str {
        match self {
            // Enums are auto imported here until end of block
            Apple => "green",
            Banana => "yellow",
            Blueberry => "blue",
            Grape => "purple",
        }
    }
}

Example 2

set_wireless_config({
    // Look Ma, no dot
    wlan: AccessPoint,
    bluetooth: Enabled
})

Why even have the dot, if the goal is to be minimal?

3 Likes

Because this already means something (suprising) in current Rust.

1 Like

Ok, but isn't it possible to create a new context, so I don't have to type . four times to appease the compiler? E.g.

auto match self {
     // Enums are auto imported here until end of block
    Apple => "green",
    Banana => "yellow",
    Blueberry => "blue",
    Grape => "purple",
}

The unfortunate status quo is that, any ident on a match case resolves to a catch all pattern (instead of matching the actual variant!). Adding some keyword to create a new special match is too complex, it makes how the match arm resolving depends on context. Apple resolves to _::Apple and Apples/apple resolves to catch all. And you can't really just disable this behaviour since this will make match arm not a simple pat fragment.

It's quite a mess tbh. Adding a dot would be the most simple change, if we want any change at all.

1 Like

Sure, but aren't there mechanisms to prevent this in a new context like auto match (syntax pending)? Not pat but pat2026 where for example you can't start a variable/capture name with a Capital letter.

  • Yes
  • Although I haven't actually used it, I've seen the proposal before and I think it looks pretty natural.
  • There are probably edge cases where it would, but in general no.
2 Likes

It's not something I would actively consider adding myself, but I would probably use it if I wake up someday and see that this syntax got added O.O

2 Likes

Rust has no language rules that care about casing, and this probably isn't going to change.

It just has conventions, which you can turn off where appropriate. (Typically in FFI where you're matching external definitions that use different conventions.)


Note also that it's already considered confusing that MAX => ... might be a binding and might be a constant. (This has been a problem since 1992 or earlier, long predating Rust's use of this syntax.)

So making that problem more common is probably not a great idea.


And of course the other reason is that this should work in more places than just patterns. For example,

options.compression_level = .Fast;

should have something to distinguish it as a type-inferred variant (rather than, say, struct Fast;).

3 Likes

Thanks for the detailed feedback. These are fair concerns.

For readability and review impact, I agree that patterns like the following introduce ambiguity.

match self {
    (.Disabled, .Disabled) => {},
    (.Enabled, .Auto) => {},
    (_, .Enabled) => {},
}

This is a real tradeoff. Leading-dot syntax optimizes for when types are clear, but in some cases it might make code review harder. This is a cost that has been acknowledged in the RFC.

As for generic paths and specialization, the example you gave would not compile. The plan is to disallow generics in leading-dot syntax, and that would make the code look something like the following.

fn main() {
    foo::<MyOpt<u32>>(.None)
}

If we do not fully reject generics, then generics would only be supported in enums, and it would look something like this: foo(.None::MyOpt<u32>). In other words, this feature does not really interact with generics or specialization.

Finally, as for why we have the dot, it makes it clear to the compiler that a path should be inferred. Without it, we would not know whether something should come from the scope or be inferred. Take the following:

enum Fruits {
    Apple,
    Banana,
}

enum Companies {
    Apple,
    Microsoft,
}

fn do_something(fruit: Fruits) {
    use Companies::*;
    match fruit {
        Apple => { /* ... */ },
        Banana => { /* ... */ }
    };
}

In the match statement, Apple could refer to something that needs to be inferred or refer to the company Apple. Either way, regardless of the solution, this would be a significant breaking change, and it is generally the goal to avoid large breaking changes.

For structs, it is possible to not have the leading dot, but for consistency, we wanted to keep it.

  • It is required for tuple structs so the compiler can tell the difference between when a tuple struct should be inferred or just a normal tuple.
  • It is required for variants for the reasons mentioned above.

Given those constraints, I don't see a viable alternative that avoids ambiguity or breaking changes.

The goal of this feature is to provide the most power while being the least disruptive. Let me know if you have any more concerns or comments. These really do help the RFC process and allow me to better understand what needs to change.

4 Likes

Great to hear. Thanks. Others have brought up that it is going to be supported in both C# and Dart.

1 Like

i think automatic inference could easily lead to mistakes and loss of readability to me the ideal solution is to have localized use statements instead especially with editor support to automatically introduce them

3 Likes