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
Inferred types _::Enum
#3444
base: master
Are you sure you want to change the base?
Inferred types _::Enum
#3444
Conversation
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 |
In crates like use windows::{
core::*, Data::Xml::Dom::*, Win32::Foundation::*, Win32::System::Threading::*,
Win32::UI::WindowsAndMessaging::*,
}; |
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. |
First, I'm not actually on any team officially, so please don't take my comments with too much weight. That said:
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 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. |
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.
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.
Definitely not, you point out some great points and your constructive feedback is welcome. |
Personally |
text/0000-infered-types.md
Outdated
[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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 (:
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 Set A:
Set B:
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 Set B does not involve macros. Whether this works for macros like 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. |
That is so useful! Let me fix it now. |
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 fn foo<T: Default>(t: T) {}
foo::<StructImplementingDefault>(_::default()) |
oh never mind, right, we don't really need to reference the trait directly either way. |
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 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 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. |
@clarfonthey glob imports easily have name collision when using multiple globs in the same module. And it is really common with names like |
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. |
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 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 My main opinion here is that 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. |
I do think inferred types are useful when matching for brevity's sake: #[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 ... |
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());
All in all, it feels like this would add a lot of complexity and make the language less consistent and harder to learn. Footnotes
|
|
I think it's not consistent and hard to read |
This comment was marked as resolved.
This comment was marked as resolved.
@JoshuaBrest This proposal may save some typing for the I think one obvious reason is this looks too implicit, when we read a snippet code like this, I'd wondering what's the concrete I prefer to |
I agree that the |
No, the function / place where the type is being inferred already has the type. This means you don’t have to import it.
…On Jul 12, 2023 at 3:31 PM -0700, Yukang ***@***.***>, wrote:
> How is this
> match x {
> _::Variant1 => ...,
> _::Variant2 => ...,
> _::Variant3 => ...,
> _::Variant4 => ...,
> _::Variant5 => ...,
> _::Variant6 => ...,
> }
> any better than this?
> use Enum as E;
> match x {
> E::Variant1 => ...,
> E::Variant2 => ...,
> E::Variant3 => ...,
> E::Variant4 => ...,
> E::Variant5 => ...,
> E::Variant6 => ...,
> }
> I can understand how it gets more readable when used in e.g. a function call that accepts many different structs, e.g.
> fn_call(LongStructName1 {a: 1}, LongStructName2 {b: 2}, LongStructName3 {c: 3}, LongStructName4 {d: 4})
> // vs
> fn_call(_ {a: 1}, _ {b: 2}, _ {c: 3}, _ {d: 4})
> but for the case of match arms, all patterns have the same enum type and you just need to add one line of alias-import.
@JoshuaBrest
What would happen if there are multiple crates containing the Enum::Variant1, we still need to import path, right?
This proposal may save some typing for the Enum pattern-matching scenario, but this kind of syntax _ { a: 1 } _::method() seems a bad idea, any other programming language use type inference for this? Why Swift limits it only to Enum and not extend it to Struct?
I think one obvious reason is this looks too implicit, when we read a snippet code like this, I'd wondering what's the concrete type here.
I prefer to use Target::*; to import more to solve the problem of importing.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.Message ID: ***@***.***>
|
I think that even though it might be a tad too bit implicit, it is important because most structs are built with |
A struct with all public fields often just means as much as a tuple with the same values, but a |
Some structs have private fields so i think that it is important to allow this. However, if the general consensus is that this should be disallowed, I am sure that we can add this to the document. |
That's exactly my point. Private fields imply that the value contains more than just the tuple, so it is important to indicate the type explicitly so that we know that private fields are involved. In fact, I would like to extend this rule to anything that implements |
That makes sense. I will add that to the RFC. On another note, I think that that might prevent bit flags from working using this syntax. I will need to look into this. |
It's been updated. |
I wrote a pre-RFC for this some time ago which does not seem to have been mentioned here. https://internals.rust-lang.org/t/pre-rfc-inferred-enum-type/16100 You may want to check some of the discussion there and mention the that post in some regard. |
Is there anything notable? I have read it and it looks like to me the same concerns there have been brought up here. |
It seems there already was a RFC that proposed similar feature only for enums: #1949 |
@JoshuaBrest this along with others would ideally be mentioned somewhere in the RFC "prior work" or such. |
Co-authored-by: Josh Triplett <josh@joshtriplett.org>
It's been almost a year since I first opened this RFC. It still seems to need some work. As the academic year is coming to a close I finally have the time and motivation to make some progress here. Thanks for all the feedback y'all.🤞 |
I want to (again) gauge what people feel about React with emojis below this comment. |
I have mixed feelings about this. On the other hand, I can see how this has the potential to really hurt code comprehensibility. Visible type names matter a lot for that I've noticed - it's one of many reasons that I prefer statically typed languages like Rust over untyped ones like Python and Javascript. Aside from the type checking features, the type names act as a form of documentation, and when elided that falls away, even if the type checking itself remains. At the end of the day, comprehensibility needs to be prioritized over conveniences for authoring code, I think. |
I totally agree. there are situations where this type of syntax should not be used and it has been discussed in earlier parts of the thread. On the other hand, however, it really should be up to the author when and where to use this. this is really intended for places where there are hints to what the type could be. An example could be: Right now, I'm trying to gauge feedback on what I should propose. request.set_cookies_mode(_::Disabled)
// Or
request.set_cookies_mode(CookiesMode::Disabled) |
I don't think it'll hurt comprehensibility much, because the types it can infer are usually implementation details rather than important bits of the code's logic. Consider:
There is one case though where I think this can be a problem - tuple structs. When a tuple struct with public fields is used instead of a regular tuple, this usually means the tuple's type itself is meaningful. The new type pattern is a good example - inferring the type kind of defeats the purpose there. So I suggest that type inference will not work for tuple structs. If it makes sense for a tuple struct to not acknowledge the type, that struct will want to Actually, there is a second case - but its reasoning is the same. Unit structs. If someone decides to have a unit struct as an argument to a function, then they probably really want callers to type that out - and thus it should not be inferred. Of course, there are unit structs that are used for other reasons - like |
This rfc proposes changing how resolution for path works, which is fundamentally hard... I've wrote something that appears the same but much much easier to implement on internals site: https://internals.rust-lang.org/t/rewritten-proposal-about-variant/20801 |
Yours seems to support |
An interesting variant of this came up on IRLO: https://internals.rust-lang.org/t/rewritten-proposal-about-variant/20801/13?u=scottmcm Specifically, it seems like it might be interesting to support Is there a rule that might work for that? I don't think the one in the reference section currently does, since |
a rule that might make it work is where types can provide a deduction guide (loosely inspired by C++17): // in std::result
#[deduce_methods_with(T)]
pub enum Result<T, E> {
Ok(T),
Err(E),
}
pub struct MyType {
pub fn new() -> Result<MyType, Error> {
todo!()
}
}
pub fn f(v: MyType) {}
pub fn g() -> Result<(), Error> {
// decides that it needs Result<MyType, ...>,
// Result has `deduce_methods_with`, so that
// makes it check MyType too, where it finds MyType::new
f(_::new()?);
Ok(())
} |
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.
I think this is a much better and more concise syntax.
Rendered