Skip to content

Inferred types _::Enum #3444

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open

Conversation

JoshuaBrest
Copy link

@JoshuaBrest JoshuaBrest commented Jun 7, 2023

This RFC is all about allowing types to be inferred without any compromises. The syntax is as follows. For additional information, please read the bellow.

struct MyStruct {
    value: usize
}

fn my_func(data: MyStruct) { /* ... */ }

my_func(_ {
    value: 0
});

I think this is a much better and more concise syntax.

If you plan on pressing the dislike button, please leave a comment explaining your disproval. Every piece of constructive feedback helps.

Rendered

@JoshuaBrest JoshuaBrest 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
@JoshuaBrest
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.

@JoshuaBrest
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.

@JoshuaBrest
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 }

[unresolved-questions]: #unresolved-questions


A few kinks on this are whether it should be required to have the type in scope. Lots of people could point to traits and say that they should but others would disagree. From an individual standpoint, I don’t think it should require any imports but, it really depends on the implementers as personally, I am not an expert in *this* subject.
Copy link
Member

Choose a reason for hiding this comment

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

I think this question needs to be resolved before the RFC is landed, since it pretty drastically changes the implementation and behavior of the RFC.

Copy link
Author

Choose a reason for hiding this comment

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

I would love to discuss it (:

@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.

@JoshuaBrest
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".

@JoshuaBrest
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.

@JoshuaBrest
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.

@JoshuaBrest
Copy link
Author

JoshuaBrest 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

@cyqsimon
Copy link

cyqsimon commented Nov 8, 2024

@SOF3 Yeah that's fair; on second thought I retract my opposition.

That being said I do like my explicit types, so if this goes through I personally will likely refrain from using it.

@lukasvrenner
Copy link

lukasvrenner commented Jan 6, 2025

This may be difficult to distinguish from the unnamed match-all _, which has very different behavior. One is binding an unused variable and the other is matching a specific type.

math Foo {
    _ { x: 1, y: 2 } => { ... },
    _ { x: 2, y: _ } => { ... },
    _ => { ... },
}

let _ { x, y } = foo;
let _ = bar;

Currently, _ can be used for two purposes: discarding a variable or inferring type annotation. They have different meanings but that's okay because they're never used in the same place. I think it's a bad idea to allow it to be used to infer struct or enum names because that overlaps with the other, very different, usage of _.

While it might be considered more concise, I believe it hides too much and makes the code harder to understand -- especially for people unfamiliar with the codebase -- without adding much more value than having to type a few less characters.

@igotfr
Copy link

igotfr commented Jan 11, 2025

is it possible infer this way?

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

@joshtriplett
Copy link
Member

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

@lemon-gith
Copy link

Hiya, @kennytm made a note in the discussion I originally posted in that this is the RFC to be in, so I'm duplicating this here for ease of access :)

Just a thought, but what if this form of syntax were to be introduced:

match dir using Foo::Bar::Direction {
    North => { ... }
    East => { ... }
    South => { ... }
    West => { ... }
}

where using implicitly scopes Foo::Bar::Direction::*, for ease of use?

  • I don't think people will mind if the line with match gets a little longer
  • This allows for narrower scoping than a preceding use statement
  • I'm impartial to the exact keyword used, but using is somewhat akin to use and so identifies the usage more clearly?
    • @berkus brings up the possible use of in instead, which I also find quite intuitive
  • I personally don't like the keyword as for this use-case, since as tends to be reserved as a keyword for renaming things

However, this comment on #2830 brings up the very valid point of match statements that destructure composite types, and hence different (possibly conflicting) type-names.
For which I'd like to offer an extension to the syntax, to mirror type-specification in function declarations:

match (fruit, company) using (
    fruit: Foo::Fruits,
    company: Bar::Companies
) {
    (Apple, Google) => { ... }
    (Orange, Samsung) => { ... }
    (Durian, Apple) => { ... }
    _ => { ... }
}

This is another reason I would prefer a keyword like using, because it lends itself well to pluralisation.
But, I do understand that it's probably annoying to have to add a new keyword to the lexer, especially with this strange extended syntax that the parser will have to make heads or tails of.
Please see Amendment, for revised thoughts

I might be very wrong, but as far as I can tell, any super deep nesting would require either:

  • manual construction of a complicated scrutinee
    • in which case, the one that constructed that scrutinee can also construct a complicated type specification :)))
  • existing types to pull from
    • which the compiler should(?) be able to infer, since those types will have to have been explicitly defined somewhere

Well, these are my two cents on the matter (ok, a bit more than two). Like most other programmers I dislike writing more code than I need to, but I also appreciate both explicit and strong typing, and the power that that affords match statements; thus, I feel that being able to scope a type just where you need it would be a great solution to the verbosity. This is an idea that borrows from a great many other ideas, but I would love to see something added to the language to address this, either way.
And for anyone that's read all my ramblings, thank you, I appreciate it, have a lovely day ^-^

Amendment: it has occurred to me now that it would perhaps be quite annoying to implement such a complex new syntax, what I wonder is if it would not be possible to simply allow type annotations within the match line, e.g.

match (fruit, company): (Foo::Fruits, Bar::Companies)
{
    (Apple, Google) => { ... }
    (Orange, Samsung) => { ... }
    (Durian, Apple) => { ... }
    _ => { ... }
}

These type annotations (or something similar) should be able to afford the compiler the right amount of information, while still being quite simple and familiar.
The points I made above about more complicated scrutinees still hold true here, I'm just modifying the syntax a little.

@JoshuaBrest
Copy link
Author

JoshuaBrest commented Jan 14, 2025 via email

@lemon-gith
Copy link

I don’t like it. You have to know what you’re writing before you write it to use this syntax.

Interesting, please forgive my confusion, but could you please give me an example of when you would write code that you didn't know the type of?

Also, as a note, I'm not asking for this explicit typing to be enforced in all match statements, just to be permitted for those that we would like to simplify.

let fred = (fruit, company);
match fred: (Foo::Fruits, Bar::Companies) {
    (x, Google) if x != Apple => { ... },
    (Orange, Samsung) => { ... },
    y if (<y condition>) => { ... },
    _ => { ... }
}

This would also be fine, using match guards to filter things out, since the exact type of the value being passed is known, if the type can be destructured, the compiler will be able to type its constituents, too, no?

Please do let me know if I'm missing something glaringly obvious.

@idanarye
Copy link

when you would write code that you didn't know the type of?

I think "didn't know" is too strong. The way I see it, this feature is for cases where fully writing the type will be cumbersome and redundant to understanding the code.

Here is a simplistic example:

use std::collections::hash_map::Entry;
use std::collections::HashMap;

fn main() {
    let mut hashmap = HashMap::<usize, usize>::new();

    for (i, num) in [1, 2, 3, 2, 1].into_iter().enumerate() {
        match hashmap.entry(num) {
            Entry::Occupied(entry) => {
                println!("Alreayd seen {num} at index {}", entry.get());
            }
            Entry::Vacant(entry) => {
                println!("Inserting new entry for {num} - index {i}");
                entry.insert_entry(i);
            }
        }
    }
}

The important type here is HashMap. Entry is an implementation detail - it's not even pub used with it in std::collections, and you have to go into std::collections::hash_map to get it.

Does spelling out Entry (which means being forced to import it or fully qualify it) help understanding what this code does?

@igotfr
Copy link

igotfr commented Jan 23, 2025

Hiya, @kennytm made a note in the discussion I originally posted in that this is the RFC to be in, so I'm duplicating this here for ease of access :)

Just a thought, but what if this form of syntax were to be introduced:

match dir using Foo::Bar::Direction {
    North => { ... }
    East => { ... }
    South => { ... }
    West => { ... }
}

where using implicitly scopes Foo::Bar::Direction::*, for ease of use?

  • I don't think people will mind if the line with match gets a little longer
  • This allows for narrower scoping than a preceding use statement
  • I'm impartial to the exact keyword used, but using is somewhat akin to use and so identifies the usage more clearly?
    • @berkus brings up the possible use of in instead, which I also find quite intuitive
  • I personally don't like the keyword as for this use-case, since as tends to be reserved as a keyword for renaming things

However, this comment on #2830 brings up the very valid point of match statements that destructure composite types, and hence different (possibly conflicting) type-names.
For which I'd like to offer an extension to the syntax, to mirror type-specification in function declarations:

match (fruit, company) using (
    fruit: Foo::Fruits,
    company: Bar::Companies
) {
    (Apple, Google) => { ... }
    (Orange, Samsung) => { ... }
    (Durian, Apple) => { ... }
    _ => { ... }
}

This is another reason I would prefer a keyword like using, because it lends itself well to pluralisation.
But, I do understand that it's probably annoying to have to add a new keyword to the lexer, especially with this strange extended syntax that the parser will have to make heads or tails of.
Please see Amendment, for revised thoughts

I might be very wrong, but as far as I can tell, any super deep nesting would require either:

  • manual construction of a complicated scrutinee
    • in which case, the one that constructed that scrutinee can also construct a complicated type specification :)))
  • existing types to pull from
    • which the compiler should(?) be able to infer, since those types will have to have been explicitly defined somewhere

Well, these are my two cents on the matter (ok, a bit more than two). Like most other programmers I dislike writing more code than I need to, but I also appreciate both explicit and strong typing, and the power that that affords match statements; thus, I feel that being able to scope a type just where you need it would be a great solution to the verbosity. This is an idea that borrows from a great many other ideas, but I would love to see something added to the language to address this, either way.
And for anyone that's read all my ramblings, thank you, I appreciate it, have a lovely day ^-^

Amendment: it has occurred to me now that it would perhaps be quite annoying to implement such a complex new syntax, what I wonder is if it would not be possible to simply allow type annotations within the match line, e.g.

match (fruit, company): (Foo::Fruits, Bar::Companies)
{
    (Apple, Google) => { ... }
    (Orange, Samsung) => { ... }
    (Durian, Apple) => { ... }
    _ => { ... }
}

These type annotations (or something similar) should be able to afford the compiler the right amount of information, while still being quite simple and familiar.
The points I made above about more complicated scrutinees still hold true here, I'm just modifying the syntax a little.

why don't just to use as instead using or : or anything else?

@daniel-pfeiffer
Copy link

daniel-pfeiffer commented Feb 2, 2025

This has also long annoyed me. I find it should be possible to put declarative statements at the beginning of every block, like:

match (fruit, company) {
    use Foo::Fruits::*;
    use Bar::Companies::*;

    (_::Apple, Google) => { ... } // Hmmm, ambiguous example, but that’s probably rare.
    (Orange, Samsung) => { ... }
    (Durian, _::Apple) => { ... }
    _ => { ... }
}

I came here after having felt a need for generalised _ in values.

fn f_unit() -> SomeLongnamedUnitType {
    // SomeLongnamedUnitType
    _
}

fn f_struct(x: f32) -> SomeComplicatedStruct<Type> {
    // SomeComplicatedStruct::<Type> { x }
    _ { x }
}

/* Edit: commented out, because this is actually pretty much a function, not a type syntax
fn f_tuple(x: f32) -> SomeComplicatedTuple<Type> {
    //  SomeComplicatedTuple::<Type>(x)
    _(x)
} */

I guess the objective should not be to squeeze it into every edge case. Instead, accept it only where there is unambiguously only one type that _ can stand for. Else the error message would list the found alternatives.

@SciMind2460
Copy link

I don't think that _::Variant syntax is a very good idea. Instead, what about self::Variant or type::Variant? This has more keystrokes but does not compromise on readability.

@idanarye
Copy link

idanarye commented Apr 4, 2025

what about self::Variant or type::Variant

Both self and type already have established meaning in Rust:

  • self means the value that this method is bound to. Which has absolutely no relationship to what this feature is supposed to do. And to make things worse - the general assumption is that self is of type Self (or some variant of it like &Self or Pin<&mut Self>) but in this case - it's going to be a different type. Always. Because if the enum in question, by sheer coincidence, happens to be Self - then we can already use Self::Vareiant. Which is not type inferrence, but still happens to do the trick in these specific cases where self::Variant makes sense.
  • type is better than self because, at least, it's related to types (we want to infer a type). But there is a reason why we don't just use thing for all the keywords even though they are all related to things. type, in Rust, has one established meaning - defining a type alias. Which is not what this feature does.

"More verbose" does not automatically mean "more readable", and using a word instead of a symbol is not enough - the chosen word also has to have the correct meaning. The reason why _ is a top candidate is not that it's short (there are other short symbols that can be used - e.g. $ or ~) - it's that _ already has a meaning in Rust, and that meaning is exactly what we want.

Consider this expression:

iter_stuff()
    .flat_map(|item| {
        if let _::Variant(payload) = item {
            Some(payload)
        } else {
            None
        }
    })
    .collect::<Vec<_>>()

_ is used twice here - first for this new feature suggested here and then for an already established Rust feature. And both usages mean the exact same thing: "you, the compiler, already know what type is allowed to go here, so instead of forcing me to spell it our please just infer it". If you are familiar with the latter (which is a very common Rust syntax) you should not be surprised by the former. On the contrary - it should be at least a little bit surprising that it doesn't already work.

@lolbinarycat
Copy link
Contributor

Yeah, I'm not too sure on the feature itself, but if we do want to add this, this seems like the correct syntax.

@rodrimati1992
Copy link

rodrimati1992 commented Apr 5, 2025

what about self::Variant

self::Variant already has a meaning, it means the item Variant in the current module.

Example:
https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=bd42f7956ed621ab4b3f483861fe39cf

const Variant: Enum = Enum::Foo(3);

#[derive(PartialEq)]
enum Enum {
    Foo(u32),
    Variant,
}

impl Enum {
    fn printit(self) {
        match self {
            Enum::Variant => { println!("Enum::Variant") }
            self::Variant => { println!("self::Variant") }
            Enum::Foo(x) => { println!("Enum::Foo({x})") }
        }        
    }
}

fn main() {
    Enum::Foo(3).printit();
}

the above runs the self::Variant branch

@JoshuaBrest
Copy link
Author

I don't think that _::Variant syntax is a very good idea. Instead, what about self::Variant or type::Variant? This has more keystrokes but does not compromise on readability.

What about it is not good? Just curious.

@lemon-gith
Copy link

Addressing previously made points

To address @igotfr's point about the use of other keywords, the as keyword is used in rust to cast between types, which is not what we're doing with this syntax.
Instead, keywords like using or the colon (:), are both 'keywords' that relate to this syntax more directly:

  • using would be a modification on use, somewhat like a temporary import
    • inspired by python's with and c++'s using
  • : {type} is now my preferred keyword: it mirrors the type specification syntax that we're already familiar with in rust
    • it's also more true to my suggested syntax: the scrutinee has one type, primitive/compound/etc., we're just re-specifying what that type is

To address @daniel-pfeiffer here, I feel that putting the generic use statements at the top of the match-statement without binding typing to specific variables, continues to introduce ambiguity in more complex structures. As you correctly identified in your example, it's now the responsibility of the user to know that there are ambiguous variants between both imported enums: in my opinion, the user shouldn't have to need to know all possible variant clashes.


Underscore and Colon Syntaxes

To speak to the response from @idanarye: you make a very good point about the existing meaning of the _ syntax in rust and how the compiler should already be able to type the scrutinee and match arm destructurings, even without any special syntax (in theory). I don't have much else to say, it's a good point :)

Nonetheless, there are 2 reasons I still prefer the type specification syntax:

A less important reason is that it moves the extra syntax from the match arms to the match head (?); that way, the extra verbosity is concentrated in one place, and doesn't need to be repeated, i.e. bite the bullet declaring your type at the top, then don't worry about the syntax after that.

The much more important reason, is that the syntax here serves 2 purposes:

  1. let the compiler know it needs to focus on the typing here (as with the _:: syntax)
  2. let the reader know what types you're working with here

That is to say, by re-declaring the types you're working with, just above a complex operation involving those types, it makes it easier to understand the code without flitting back and forth; seeing _::, in my opinion, just tells the reader that it's possible to figure out the typing, if they so wish.
It could be said that it increases the spatial locality of the type annotation for readers.

Naturally, if one didn't want to write out a type (or sub-sections for a more complex type), it would make sense to allow _ in the type specification, e.g. match fred: (Foo::Fruits, _) {...}, since these notations are not mutually exclusive.

To add to that, the colon syntax is very specific to match-like constructs, whereas I can see the underscore being very useful in a lot of places, described by many comments in this RFC, back in 2023 and early 2024.

Of course, regarding the compiler, I imagine that the _:: syntax would likely be easier to implement?


Match Statement Debate Overview (to the best of my knowledge)

Just to give a full example:

// import this enum from somewhere
use idk::Bar;  // use idk::Bar::{operation, ..., Companies};  // may be better
// define this enum here
enum Fruits {
    Apple,
    Orange(u32),
    Durian,
    Cantaloupe
}

let fruit: Fruits = { ... };
let company = { ... };  // some operation related to imported `Bar` module

let fred = (fruit, company);

The existing syntax (without workarounds like scoped imports):

match fred {
    (Fruits::Apple, Bar::Companies::Google) => { ... }
    (Fruits::Orange(x), Bar::Companies::Samsung) if x < 7 => { ... }
    (Fruits::Orange(_), x) if !matches!(x, Bar::Companies::Samsung) => { ... }
    (Fruits::Durian, Bar::Companies::Apple) => { ... }
    _ => { ... }
}

This is the underscore syntax (I believe?) that has been suggested:

match fred {
    (_::Apple, _::Google) => { ... }
    (_::Orange(x), _::Samsung) if x < 7 => { ... }
    (_::Orange(_), x) if !matches!(x, _::Samsung) => { ... }
    (_::Durian, _::Apple) => { ... }
    _ => { ... }
}

This is the colon syntax that I'm suggesting:

match fred: (Fruits, Bar::Companies) {
    (Apple, Google) => { ... }
    (Orange(x), Samsung) if x < 7 => { ... }
    (Orange(_), x) if !matches!(x, Samsung) => { ... }
    (Durian, Apple) => { ... }
    _ => { ... }
}

To note, tuple variants under underscore syntax, like _::Orange(_), would be valid, semantically correct syntax, which doesn't break anything, but there's just something to be said about juxtaposing two different usages of the same symbol 1.

If we get into the point that only ambiguous variants should use _::, I'd like to raise the point that Orange is also a company2 and would then also require disambiguation, but you can't tell that just by looking at the code here.

I think the heart of the problem is that we're happy to have imported things beforehand, and to have properly qualified our types, but not to keep qualifying them again and again in match arms.


This RFC and Implementation

One benefit of all these suggestions is to reduce glob imports, which @JoshuaBrest mentions near the start of this issue. I agree with both sides, but side more with @JoshuaBrest:

  • glob imports are alright when importing from well-written crates with good publishing hygiene
    • because they often include helper functions that you may not have realised you need to import
    • sometimes they can be necessary when importing from crates that extend functionality, implicitly
  • glob imports are generally bad, because most users don't check everything they're importing
    • as a cybersecurity person: whitelisting is just usually better practice than blacklisting
    • a general "import what you need" philosophy often helps to reduce clashes and ambiguities

I (also?) agree with the notion that the underscore syntax is a good idea, but I do prefer the extra (and more spatially localised) information conveyed to the reader by the colon syntax, which I think is the main benefit of the non-underscore syntaxes that people are suggesting.
I am, of course, slightly biased, and more than open to other thoughts regarding potential flaws and edge cases that the colon syntax can't handle, as well as worries of increasing the implementation complexity.

P.S. sorry for the massive essay, I'm just hoping to summarise some of the key discussions here

Footnotes

  1. also, !matches!() is very interesting, though I mainly find it amusing :}

  2. which is why I picked that as the second fruit

@idanarye
Copy link

idanarye commented Apr 8, 2025

Nonetheless, there are 2 reasons I still prefer the type specification syntax:

...

The much more important reason, is that the syntax here serves 2 purposes:

1. let the compiler know it needs to focus on the typing here (as with the `_::` syntax)

Why would the compiler need to "focus"? It's not a human - it does not have a limited attention span that needs help limiting itself to the important things.

2. let the reader know what types you're working with here

That is to say, by re-declaring the types you're working with, just above a complex operation involving those types, it makes it easier to understand the code without flitting back and forth; seeing _::, in my opinion, just tells the reader that it's possible to figure out the typing, if they so wish. It could be said that it increases the spatial locality of the type annotation for readers.

The reader, on the other hand, does need to focus. And so does the writer. I think, in a match statement (TBH - pretty much in any place where this feature is relevant) the type is not what one needs to focus on - it's the variants. Just like omitting the type when you use combinators (where would one even write the types when using combinators? EXACTLY!) doesn't hurt readability, so does inferring the types in these cases.

Naturally, if one didn't want to write out a type (or sub-sections for a more complex type), it would make sense to allow _ in the type specification, e.g. match fred: (Foo::Fruits, _) {...}, since these notations are not mutually exclusive.

I'm confused - wouldn't omitting the type there mean you have to write it in every branch of the match?

This is the colon syntax that I'm suggesting:

match fred: (Fruits, Bar::Companies) {
    (Apple, Google) => { ... }
    (Orange(x), Samsung) if x < 7 => { ... }
    (Orange(_), x) if !matches!(x, Samsung) => { ... }
    (Durian, Apple) => { ... }
    _ => { ... }
}

I don't like this syntax for the following reasons:

  1. I think the rules are too complicated. From the rest of what you wrote, I understand that the : (Fruits, Bar::Companies) brings the variants of Fruits and Bar::Companies into scope - but only in the places where they'd fit into the patterns of destructuring fred? But the matches!(x, Samsung) bit implies it gets extended to items bound into these pattern? How far does it go? Can I use the other variants Bar::Companies inside the branch block itself, as long as it refers to x?
  2. When use Fruits::*; brings Apple, Orange and Durian into scope, these names are represented by the *. With just Fruits, what represents the variants?
  3. It's much less versatile than _. Even in the scope of match - consider if Fruits::Orange's payload was an struct Orange { ... }:
    match fred: (Fruits, _) {
        (Orange(Orange { size, firmness }), _) => { ... }
        _ => { ... }
    }
    We have to repeat the word Orange twice (because no one brought the Orange struct into scope) whether with the _ syntax we can infer that too since it's the type of the variant's payload:
    match fred {
        (_::_Orange(_ { size, firmness }), _) => { ... }
        _ => { ... }
    }

To note, tuple variants under underscore syntax, like _::Orange(_), would be valid, semantically correct syntax, which doesn't break anything, but there's just something to be said about juxtaposing two different usages of the same symbol.

This is already supported - e.g. one could use Some::<_>(_) as a pattern.

If we get into the point that only ambiguous variants should use _::, I'd like to raise the point that Orange is also a company and would then also require disambiguation, but you can't tell that just by looking at the code here.

There is no ambiguity because the _::Orange resolution does not start from Orange. It starts from _. The compiler looks at _ and says "there should be a constructor pattern here - but of which type?" and uses the other information (type of first item of fred) to infer that type. Once it knows the type, it replaces _ with it - and only then looks at the ::Orange. At no point does it care that other enums also have an Orange variant.

I think the heart of the problem is that we're happy to have imported things beforehand, and to have properly qualified our types, but not to keep qualifying them again and again in match arms.

I'm not happy with either of these things. I don't want to pollute the namespace with something the compiler can easily infer for me, and more importantly - having to needlessly acknowledge types decreases the power of macros (which can't always determine the type)

@kennytm
Copy link
Member

kennytm commented Apr 9, 2025

: {type} is now my preferred keyword: it mirrors the type specification syntax that we're already familiar with in rust

afte type-ascription got nuked from orbit (#3307) I'm quite certain the expr: Type syntax will never pass T-lang.

@lemon-gith
Copy link

Colon and Underscore Syntax Flaws

Readability is partially about how quickly a reader can understand code; reducing effort on the writer's side generally increases the effort on the reader's side, and rust is a language that's designed for maintability and sacrificing write-time (be that for run-time or for read-time).

  1. When use Fruits::*; brings Apple, Orange and Durian into scope, these names are represented by the *. With just Fruits, what represents the variants?

Nothing represents the variants, the whole enum is 'brought into scope', which is in line with the way that pub enum is handled in rust, as opposed to pub struct.

  1. It's much less versatile than _. Even in the scope of match - consider if Fruits::Orange's payload was an struct Orange { ... }: [...] We have to repeat the word Orange twice.

Firstly, I did admit that the _-syntax is indeed more versatile, and that I'm only advocating for a different syntax for match statements, specifically.
Secondly, in the example you put forwards (below), I'm actually completely in favour of repeating Orange twice. That is because each Orange refers to a completely different type, and therefore should be qualified individually.

I don't mean this in an antagonistic way, but could you really tell me if you saw:

match fred {
    (_::Orange(_ { size, firmness }), _)

in an unfamiliar codebase, that it would be easier for you to understand than:

match fred using (Fruits, b::C) { // qualifying:
    (Orange(Orange { size, firmness }), _)

The former tells me that there's an enum (? I'm guessing) somewhere, with an Orange variant, holding a struct with a name.
The latter tells me that there's an enum called Fruits, and that its Orange variant holds a struct, also called Orange.

Moreover, if I want to find its definition, I'd know that Fruits is either:

  • fully qualified by a use statement somewhere in this module
  • defined within this module
  • or were it std::foo::Fruits (e.g.), that's already its full qualification path

Though the compiler would likely be fine with either, as the reader, you understand what's going on much quicker with the latter syntax.

This is already supported - e.g. one could use Some::<_>(_) as a pattern.

This is a good example, I don't particularly like that pattern, but if it is part of the language, then my previous point is rendered moot.

after type-ascription got nuked from orbit (#3307) I'm quite certain the expr: Type syntax will never pass T-lang.

Thank you @kennytm, I wasn't aware: there are good points in there. Independently, I've also realised another problem with the colon syntax: it is a lie, i.e. it's misleading as to what is happening under-the-hood.


Proposing regression to using keyword

@idanarye, thank you for highlighting a very important point here, which is that type specification is indeed not the proper way to qualify these match heads.
Admittedly, I think I was just drawn in by how clean and familiar the colon syntax was, but you're absolutely right to point out that type specification is not actually what the colon syntax translates to under-the-hood: that syntax would be misleading. I was wrong to pick that syntax.

Instead of editing my previous comment, and distorting a good debate, I'll put this here1:

match fred using (Fruits, bar::Companies) {
    (Apple, Google) => { ... }
    (Orange(x), Samsung) if x < 7 => { ... }
    (Orange(_), x) if !matches!(x, Samsung) => { ... }
    (Durian, Apple) => { ... }
    _ => { ... }
}

My suggestion here, is that this would translate to path expansion under-the-hood, e.g. Google -> bar::Companies::Google. In something akin to, but not exactly like, the use keyword (hence using).

Based on our previous discussions of what _:: can do as is, I think we can agree that it would be possible for the compiler to fill in this much information, given what it already knows and the qualifications provided by the using clause, e.g. expanding Apple to Fruits::Apple given that it's being compared to sub-variable of type Fruits.

In this Rust Playground, I've written a crude example of what a compiler expansion could look like.

Going Forwards (match statements)

I also feel like using syntax still isn't an optimal solution, since it would require the introduction of a new keyword that has a very limited use-case.

Warning

It may be a good idea to separate the discussions surrounding:

  1. _:: syntax for imports and inferred types
  2. more concise match patterns

since I think the solutions may differ slightly in their optimal implementations.

To bring us back to square one, I'd like to consider what our goal is (for match), i.e. what the ideal syntax would look like for match statements:

match fred {
    (Apple, Google) => { ... }
    (Orange(x), Samsung) if x < 7 => { ... }
    (Orange(_), x) if !matches!(x, Samsung) => { ... }
    (Durian, Apple) => { ... }
    _ => { ... }
}

In my head, this would be perfect:

  • no boilerplate on the match head
  • match arms as simple as they can be
  • type-continuity within match arms, for match guards

I think it would be a good idea to define what we're working towards, and then from there decide what we're willing to compromise on, in order to achieve a result as close to it as possible.

Footnotes

  1. I've lowercased Bar to bar, to conform to module naming practices, since it was just a placeholder

@idanarye
Copy link

Readability is partially about how quickly a reader can understand code; reducing effort on the writer's side generally increases the effort on the reader's side, and rust is a language that's designed for maintability and sacrificing write-time (be that for run-time or for read-time).

The way I see it, readability is not about info-dumping as much as possible on the reader. Omitting information only hurts readability if the omitted information is required for understanding the code (the flow of the code - not every little decision the compiler is going to make when compiling it)

My argument is that in cases handled by this feature1, this information is not required. I'll explain with the example you gave:

I don't mean this in an antagonistic way, but could you really tell me if you saw:

match fred {
    (_::Orange(_ { size, firmness }), _)

in an unfamiliar codebase, that it would be easier for you to understand than:

match fred using (Fruits, b::C) { // qualifying:
    (Orange(Orange { size, firmness }), _)

The former tells me that there's an enum (? I'm guessing) somewhere, with an Orange variant, holding a struct with a name. The latter tells me that there's an enum called Fruits, and that its Orange variant holds a struct, also called Orange.

In both versions, I know that I'm handling the Orange case of fred.0 and I know that I'm using the size and firmness values of that case. I don't know what the types of size and firmness are in either version, and no one suggest we have to spell these types here - even though we are probably going to use size and firmness inside the branch's block.

No version tells me what the Orange case means. Knowing that the name of the type inside it is Orange provides no additional insight ("orange is orange". Brilliant). The documentation comment of Orange - either one in the variant or one in the struct - could have explained what "orange" means - but I hope no one suggest we should force developers to copy-paste the documentation comments to patterns?

The second version tells me that there is a struct named Orange. So what? I'm not using that struct. I'm using its fields. When reading this code, I don't care which methods this struct supports, because there is no binding of type Orange here that can use these methods. That type would be of interest had it not been destructured:

match fred using (Fruits, b::C) { // qualifying:
    (Orange(orange), _) => { ... }

Because then we could do orange.some_orange_method() inside the block. But in this case - even current Rust syntax that does not have this feature does not force us (or even make it possible, at least without rebinding it inside the block) to acknowledge that type.

Fruits is similar. We don't use fred.0 as a Fruits - we destruct it immediately. Why is acknowledging the type more important when accessing a variant than when accessing a method? If anything, I'd say it's less important because you want to focus on the variant.

Footnotes

  1. One exception of this rule is the newtype pattern, which can be solved by either linting the usage of this feature with single-item tuple structs or by disabling it for tuple structs entirely (either way - this limitation should only apply for tuple structs. Tuple variants of enums should still be able to use the feature)

@tmccombs
Copy link

Ideally, I think the solution for this should work in all pattern contexts, not just match expressions. That would include things like if let, while let, let/else, macros like matches!, etc.

The using syntax (and : syntax in the scrutinee part) don't really work for that.

@idanarye
Copy link

I don't think the using syntax is even meaningful for other patterns. match is the only one where the type is repeated. I don't see how this:

if let Orange(orange) = fruit using Fruits {
    ...
}

Is better than this:

if let Fruits::Orange(orange) = fruit {
    ...
}

@JoshuaBrest
Copy link
Author

I wrote this PR when I was much younger and more naive. Looking back, I realize there are definitely areas that need revision.

One major concern I’ve been reflecting on is how we resolve the base type when using _::. A core principle of Rust is explicitness, especially around imports; developers are expected to use every type they interact with. But this proposal introduces what could be seen as “ghost imports,” where a type can be referenced and instantiated without being explicitly brought into scope.

This is a pretty fundamental shift from how Rust usually operates. It creates a case where types are being used without a visible import path, which could affect both readability and tooling support. For example, it becomes harder to track type usage by grepping for its name or relying on IDE tooling, since _ hides the identity of the type.

One possible solution is to require that the type behind _:: must already be in scope via a use statement (similar in spirit to the using keyword proposal). While this would keep type usage explicit, it may reduce the ergonomics and conciseness the proposal aims to provide. It also introduces the odd case where something is technically used but appears unused in the code.

These are just some of my thoughts. I don’t have a fully formed solution yet, but I’d love to hear your input so I can revise the RFC accordingly.

@idanarye
Copy link

But this proposal introduces what could be seen as “ghost imports,” where a type can be referenced and instantiated without being explicitly brought into scope.

Rust can already do that - e.g. with Default::default().

@JoshuaBrest
Copy link
Author

Whoops! You’re right! Yet another reason why this change is not problematic!

@bbb651
Copy link

bbb651 commented Apr 23, 2025

Proposing regression to using keyword

@idanarye, thank you for highlighting a very important point here, which is that type specification is indeed not the proper way to qualify these match heads.
Admittedly, I think I was just drawn in by how clean and familiar the colon syntax was, but you're absolutely right to point out that type specification is not actually what the colon syntax translates to under-the-hood: that syntax would be misleading. I was wrong to pick that syntax.

Instead of editing my previous comment, and distorting a good debate, I'll put this here1:

match fred using (Fruits, bar::Companies) {
    (Apple, Google) => { ... }
    (Orange(x), Samsung) if x < 7 => { ... }
    (Orange(\_), x) if !matches!(x, Samsung) => { ... }
    (Durian, Apple) => { ... }
    \_ => { ... }
}

My suggestion here, is that this would translate to path expansion under-the-hood, e.g. Google -> bar::Companies::Google. In something akin to, but not exactly like, the use keyword (hence using).

Based on our previous discussions of what _:: can do as is, I think we can agree that it would be possible for the compiler to fill in this much information, given what it already knows and the qualifications provided by the using clause, e.g. expanding Apple to Fruits::Apple given that it's being compared to sub-variable of type Fruits.

In this Rust Playground, I've written a crude example of what a compiler expansion could look like.

Doesn't this have the exact same problems as use Enum::*? If I remove Fruits::Apple, suddenly Apple in the pattern is now an identifier, which will make the pattern match anything silently without an error (only an unused variable warning, and maybe an unreachable pattern warning).
_::Apple on the other hand is explicitly an enum variant and will to compile.

I also don't like the syntax, firstly I think using should be replaced with use: using is not a keyword so we either have to reserve it which is a ton of churn and needs to wait for an edition or make it a "soft keyword" which is bad for tooling, especially simpler syntax highlighting. More importantly, it forces you to opt-in to it in a way that's global to the match rather than local to the pattern, which is bad in general - it makes it harder to reason about large matchs without seeing the start (which is where this is most useful), to copy code, to correlate the changes in diffs, etc. And it's incompatible with other contexts patterns are used.

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.