-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Path Inference #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
JoshBashed
wants to merge
2
commits into
rust-lang:master
Choose a base branch
from
JoshBashed:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+384
−0
Open
Path Inference #3444
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,384 @@ | ||
| - Feature Name: Path Inference | ||
| - Start Date: 2023-06-06 | ||
| - RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) | ||
| - Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) | ||
|
|
||
| ## Summary | ||
| [summary]: #summary | ||
|
|
||
| This RFC introduces the leading-dot syntax for path inference in type construction. When the type is known from context, developers can write `.Variant`, `.Variant { field: 1 }`, and `.Variant(1)` for enums and `.{ field: 1 }` and `.(1)` for structs instead of writing out the type name. | ||
|
|
||
| ## Motivation | ||
| [motivation]: #motivation | ||
|
|
||
| When working with enums in match statements or other contexts where variants are used repeatedly, developers commonly use glob imports (`use Enum::*`) or single-letter aliases (`use Enum as E`) to reduce verbosity. Glob imports risk name collisions, and single-letter aliases are disallowed in some codebases. Leading-dot syntax provides a standardized alternative that avoids both problems while maintaining clarity about where types come from. | ||
|
|
||
| ```rust | ||
| // Current Approach (glob import) | ||
| use FooBar::*; | ||
| match my_enum { | ||
| Foo => ..., | ||
| Bar => ..., | ||
| } | ||
|
|
||
| // Current Approach (single letter import) | ||
| use FooBar as F; | ||
| match my_enum { | ||
| F::Foo => ..., | ||
| F::Bar => ..., | ||
| } | ||
|
|
||
| // Proposed Approach | ||
| match my_enum { | ||
| .Foo => ..., | ||
| .Bar => ..., | ||
| } | ||
| ``` | ||
|
|
||
| Function calls with struct parameters also benefit from this syntax. Named parameters in functions have been proposed multiple times for Rust. Leading-dot syntax for structs achieves a similar goal. Combining it with default field values only makes it more comparable. | ||
|
|
||
| ```rust | ||
| fn my_function(my_struct: MyStruct) { ... } | ||
|
|
||
| // Current Approach | ||
| my_function(MyStruct { field: 1 }); | ||
|
|
||
| // Proposed Approach | ||
| my_function(.{ field: 1 }); | ||
| ``` | ||
|
|
||
| ## Guide-level explanation | ||
| [guide-level-explanation]: #guide-level-explanation | ||
|
|
||
| When the compiler knows what type to expect, instead of writing the full type, it's possible to write the type using the leading-dot syntax. | ||
|
|
||
| ### All forms | ||
|
|
||
| - **Enum Variant (unit):** `.Variant` | ||
| - **Enum Variant (tuple):** `.Variant(1)` | ||
| - **Enum Variant (named fields):** `.Variant { value: 1 }` | ||
| - **Struct with (named fields):** `.{ value: 1 }` | ||
| - **Struct with (tuple):** `.(1)` | ||
|
|
||
| ### Enum Variants | ||
|
|
||
| ```rust | ||
| enum Status { | ||
| Pending(f64), | ||
| Complete { data: Vec<u8> }, | ||
| Failed | ||
| } | ||
|
|
||
| fn update_status(s: Status) { /* ... */ } | ||
|
|
||
| // Current Approach | ||
| fn get_default_status() -> Status { | ||
| Status::Pending(0.0) | ||
| } | ||
| update_status(Status::Pending(0.5)); | ||
| update_status(Status::Complete { data: vec![1, 2, 3] }); | ||
| update_status(Status::Failed); | ||
|
|
||
| // Proposed Approach | ||
| fn get_default_status() -> Status { | ||
| .Pending(0.0) | ||
| } | ||
| update_status(.Pending(0.5)); | ||
| update_status(.Complete { data: vec![1, 2, 3] }); | ||
| update_status(.Failed); | ||
| ``` | ||
|
|
||
| ### Match Arms | ||
|
|
||
| ```rust | ||
| enum Status { | ||
| Pending(f64), | ||
| Complete { data: Vec<u8> }, | ||
| Failed | ||
| } | ||
|
|
||
| // Current Approach | ||
| match status { | ||
| Status::Pending(progress) => ..., | ||
| Status::Complete { data } => ..., | ||
| Status::Failed => ..., | ||
| } | ||
|
|
||
| // Proposed Approach | ||
| match status { | ||
| .Pending(progress) => ..., | ||
| .Complete { data } => ..., | ||
| .Failed => ..., | ||
| } | ||
| ``` | ||
|
|
||
| ### Struct Construction | ||
|
|
||
| ```rust | ||
| struct Location(f64, f64); | ||
| struct WeatherData { | ||
| location: Location, | ||
| humidity: f64, | ||
| } | ||
|
|
||
| fn print_weather_data(weather_data: WeatherData) { /* ... */ } | ||
|
|
||
| // Current Approach | ||
| fn get_default_weather_data() -> WeatherData { | ||
| WeatherData { | ||
| location: Location(0.0, 0.0), | ||
| humidity: 0.5, | ||
| } | ||
| } | ||
| print_weather_data(WeatherData { | ||
| location: Location(0.0, 0.0), | ||
| humidity: 0.5, | ||
| }); | ||
|
|
||
| // Proposed Approach | ||
| fn get_default_weather_data() -> WeatherData { | ||
| .{ | ||
| location: .(0.0, 0.0), | ||
| humidity: 0.5, | ||
| } | ||
| } | ||
| print_weather_data(.{ | ||
| location: .(0.0, 0.0), | ||
| humidity: 0.5, | ||
| }); | ||
| ``` | ||
|
|
||
| ## Reference-level explanation | ||
| [reference-level-explanation]: #reference-level-explanation | ||
|
|
||
| ### Syntax Changes | ||
|
|
||
| ```diff | ||
| PathInExpression ::= | ||
| "::"? PathExprSegment ( "::" PathExprSegment )* | ||
| + | "." IDENTIFIER | ||
|
|
||
| StructExpression ::= | ||
| - PathInExpression "{" ( StructExprFields | StructBase)? "}" | ||
| + ( PathInExpression | "." ) "{" ( StructExprFields | StructBase)? "}" | ||
|
|
||
| CallExpression ::= | ||
| - Expression "(" CallParams? ")" | ||
| + ( Expression | "." ) "(" CallParams? ")" | ||
|
|
||
| StructPattern ::= | ||
| - PathInExpression "{" StructPatternElements? "}" | ||
| + ( PathInExpression | "." ) "{" StructPatternElements? "}" | ||
|
|
||
| TupleStructPattern ::= | ||
| - PathInExpression "(" TupleStructItems? ")" | ||
| + ( PathInExpression | "." ) "(" TupleStructItems? ")" | ||
| ``` | ||
|
|
||
| ### Type Resolution | ||
|
|
||
| Path inference resolves the inferred path based on the concrete type expected at an expression or pattern position. These shall include return type annotations, function parameters, variable type annotations, or parent expressions. | ||
|
|
||
| ### Scoping and Privacy | ||
|
|
||
| Path inference respects Rust's normal privacy rules. If a type is private, it remains inaccessible using path inference; that is to say that the leading dot syntax does not grant any additional access to otherwise inaccessible types. | ||
|
|
||
| ## Drawbacks | ||
| [drawbacks]: #drawbacks | ||
|
|
||
| ### Ambiguity in Functions with Multiple Enum Parameters | ||
|
|
||
| When multiple parameters share variant names, the leading-dot syntax can be confusing and ambiguous. | ||
| ```rust | ||
| enum RadioState { Disabled, Enabled } | ||
| enum WifiConfig { Disabled, Reverse, Enabled } | ||
|
|
||
| fn configure_wireless(radio: RadioState, wifi: WifiConfig) { /* ... */ } | ||
|
|
||
| configure_wireless(.Disabled, .Enabled); | ||
| ``` | ||
|
|
||
| Without looking at the function signature, it's unclear which argument corresponds to which parameter. This is problematic during code review. | ||
|
|
||
| **Note:** Developers can use explicit types when they deem it to be important. They can also restructure APIs to use struct parameters where field names provide more context. The below example is much clearer. | ||
| ```rust | ||
| struct WirelessConfig { | ||
| radio: RadioState, | ||
| wifi: WifiConfig, | ||
| } | ||
|
|
||
| configure_wireless(.{ radio: .Disabled, wifi: .Enabled }); | ||
| ``` | ||
|
|
||
| ### Hidden Type Information in Code Review | ||
|
|
||
| When reading diffs or reviewing code without an IDE, the inferred type is not immediately visible. The below example might be difficult to understand. | ||
| ```rust | ||
| configure(.{ | ||
| primary: .{ | ||
| mode: .Active, | ||
| fallback: .{ strategy: .Exponential } | ||
| } | ||
| }); | ||
| ``` | ||
|
|
||
| **Note:** Ideally, the API designer should use more explicit function and field names, but this is not always possible. | ||
|
|
||
| ## Rationale and alternatives | ||
| [rationale-and-alternatives]: #rationale-and-alternatives | ||
|
|
||
| ### Dot `.` over underscore `_` | ||
|
|
||
| In Rust, the underscore `_` is already used as a placeholder for type inference in type parameters and type arguments. As such, reusing `_` for path inference was considered. In the end, however, it was decided that this solution was not ideal and that dot syntax would be a better fit. | ||
|
|
||
| The main issue with underscores is that an underscore can be part of an identifier. As such, it's not possible to write `_Variant` the same way as `.Variant`. It would have to be written as `_::Variant`. This syntax is much longer and resembles a partially known type rather than an inferred type. In addition, a developer might think that it is okay to write `std::_::Variant`. | ||
|
|
||
| Additionally, the underscore `_` already has many meanings, such as discarding an unused variable or acting as a placeholder. Adding on additional meanings to the underscore would reduce clarity rather than increase consistency. | ||
|
|
||
| Leading dot syntax clearly communicates that the entire path is being inferred from context, and it's shorter. | ||
|
|
||
| ## Prior art | ||
| [prior-art]: #prior-art | ||
|
|
||
| ### `Default::default()` | ||
|
|
||
| Rust already has a form of type inference for construction: `Default::default()`. When the type is known from context, it's possible to write: | ||
|
|
||
| ```rust | ||
| fn get_config() -> Config { | ||
| Default::default() | ||
| } | ||
|
|
||
| let settings: Settings = Default::default(); | ||
| ``` | ||
|
|
||
| People have also previously implemented path inference in a macro using `Default::default()`. | ||
|
|
||
| ```rust | ||
| macro_rules! s { | ||
| ( $($i:ident : $e:expr),* ) => { | ||
| { | ||
| let mut temp = Default::default(); | ||
| if false { | ||
| temp | ||
| } else { | ||
| $( | ||
| temp.$i = $e; | ||
| )* | ||
| temp | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[derive(Debug, Default)] | ||
| struct Foo { x: i32, y: u32 } | ||
|
|
||
| fn takes_foo(x: Foo) { dbg!(x); } | ||
|
|
||
| takes_foo(s! { x: 123, y: 456 }); | ||
| ``` | ||
|
|
||
| ### Swift | ||
|
|
||
| Swift, the main inspiration for this RFC, has had leading-dot syntax since its initial release in 2014. It's widely used throughout Swift codebases: | ||
|
|
||
| ```swift | ||
| enum Status { | ||
| case pending(f64) | ||
| case complete(Data) | ||
| } | ||
|
|
||
| struct Data { | ||
| var foo: String | ||
| } | ||
|
|
||
| func make_data() -> Data { | ||
| .init(foo: "bar") | ||
| } | ||
|
|
||
| func make_status() -> Status { | ||
| .pending(0.5) | ||
| } | ||
| ``` | ||
|
|
||
| This is functionally equivalent to the following: | ||
|
|
||
| ```swift | ||
| func make_data() -> Data { | ||
| Data(foo: "bar") | ||
| } | ||
|
|
||
| func make_status() -> Status { | ||
| Status.pending(0.5) | ||
| } | ||
| ``` | ||
|
|
||
| ## Unresolved questions | ||
| [unresolved-questions]: #unresolved-questions | ||
|
|
||
| ### Generic Arguments in Enum Variants | ||
|
|
||
| Rust permits generic arguments after the variant for enum variants. | ||
| ```rust | ||
| enum Status<Progress, Data> { | ||
| Pending(Progress), | ||
| Complete { data: Data }, | ||
| } | ||
|
|
||
| Status::Pending<f64, Foo>(0.0); | ||
| Status::Complete<f64, Foo> { data: Foo::default() }; | ||
| ``` | ||
| With leading-dot syntax, the analogous forms would be: | ||
| ```rust | ||
| .Pending<f64, Foo>(0.0); | ||
| .Complete<f64, Foo> { data: Foo::default() }; | ||
| ``` | ||
| The intent of path inference is that the path should be fully inferred from context. Allowing generic arguments doesn't make much sense because the type was supposed to be inferred already. It also creates an asymmetry; structs do not have an equivalent position for variant generics. This might be super confusing. | ||
|
|
||
| Overall, it is not clear whether supporting generics for enum variants in path inference provides meaningful ergonomic benefits. | ||
|
|
||
| ## Future possibilities | ||
| [future-possibilities]: #future-possibilities | ||
|
|
||
| ### Generic Arguments in Path Inference | ||
|
|
||
| A future syntax addition could allow generics to appear in inferred typebases, such as: | ||
|
|
||
| ```rust | ||
| use std::fmt::{Display, Debug}; | ||
|
|
||
| fn update_status<Ok: Display, Err: Debug>(s: Result<Ok, Err>) { | ||
| /* ... */ | ||
| } | ||
|
|
||
| update_status(.Ok::<u8, String>(42)); | ||
| update_status(.Err::<u8, String>("oops")); | ||
|
|
||
| // or | ||
| update_status(.<u8, String>::Ok(42)); | ||
| update_status(.<u8, String>::Err("oops")); | ||
|
|
||
| struct Foo<T>(T); | ||
| struct Bar<Data> { | ||
| data: Data, | ||
| } | ||
|
|
||
| fn foo<T>(t: T) -> Foo<T> { | ||
| Foo(t) | ||
| } | ||
| fn bar<Data>(data: Data) -> Bar<Data> { | ||
| Bar { data } | ||
| } | ||
|
|
||
| foo(.<u8>(42)); | ||
| bar(.<String>{ data: "hello".to_string() }); | ||
| ``` | ||
|
|
||
| ### Impls in Path Inference | ||
|
|
||
| Another feature could be allowing calls to associated functions. While an associated function does not have to return the value that is expected, it's possible to show a type mismatch error. This would allow developers to write: | ||
|
|
||
| ```rust | ||
| let items: Vec<Item> = .with_capacity(10); | ||
| ``` | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
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.
This isn’t the correct current syntax. It would need to be
with additional
::tokens afterPending/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?
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 asStatus::<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 areuse’d which – the need forPending::<f64, Foo>(0.0)withuse Status::Pending;– presumably motivated the existence ofStatus::Pending::<f64, Foo>(0.0)syntax in the first place.)Footnotes
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. ↩