-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
.type
to get expression type
#2706
Conversation
imagine that it would leverage the work that's already been done with `const` | ||
generics. | ||
|
||
Some cases to be aware of are non-simple expressions, such as `1 + 1`. It is |
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.
How does this interact with type inference? Currently, as I understand it, Rust allows the following code:
let mut v = Vec::new(); //1, 3
v.push(1u8); //2
Where type inference works in the following way:
v
has typeVec<?>
Vec::push()
is called with au8
. This parameter is of typeT
thereforeT
has typeu8
- Therefore
v
has typeVec<u8>
.
Now consider the following code:
let mut v1 = Vec::new();
type V = v1.type; //What is the type `V` here?
let mut v2 = V::new();
v2.push(1u16); //Does this line mean `v1` is of type `Vec<u16>`?
v1.push(1u8); //Does this line compile?
Would you expect this code to be valid? If not, what is the type of v1
? Is it Vec<u8>
because of v1.push(1u8)
or Vec<u16>
because type V = v1.type
and v2.push(1u16)
?
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.
Simplifying a bit:
let mut v1 = Vec::new();
let mut v2 = <v1.type>::new();
What should happen with v2
is that the known type of v1
at that point is used. Specifically, we know that v1: Vec<?T0>
and so this type, including the names of unsolved inference variables, is also assigned to v2
. This would use the same inference variable ?T0
for both v1
and v2
. Therefore, when we do v2.push(1u16)
we should equate ?T0 = u16
and therefore it follows that v1: Vec<u16>
wherefore v1.push(1u18);
cannot compile.
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.
Seems logical but potentially surprising to users that v1
's type was inferred because of how v2
was used.
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.
That's type inference for you... often surprising ;) E.g. we have bidirectional type-checking but folks often don't notice the difference.
We could do some alternative schemes:
-
Generate fresh variables in
expr.type
for each variable inexpr.type
.
Seems less useful? -
Error when unsolved variables are found in
expr.type
.
This entails that the full type must be known inexpr
.
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 surprising at all. There are already many, many ways to crate a situation in rust where the type of some local v1
is decided based on how another local v2
is used.
I’m not a fan of this kind of type definition. Currently you can reason about the high-level structure of code purely by looking at function signatures and type definitions. But with this change the type of something can require reading the implementation to figure out. It breaks an abstraction boundary in a way the decreases readability. I do sympathize with the motivation of using it in macros, though (it would massively clean up some macros). Perhaps a more restricted alternative would be to syntactically expose inference variables: let x: Vec<?T> = Vec::new(); // ?T is a named inference variable
let y: ?T = 0; This has a few benefits:
I would still want it to be explicitly bad style to use this anywhere except macros, though. |
In principle you can always achieve this by introducing some nested function, right?
It even sounds possible to build these with a proc macro, although not sure how that interacts whit macro evaluation? |
I wholeheartedly agree that this can lead to code that's very difficult to read and thus a headache to maintain.
I like this approach for some of the reasons you listed. How would I be able to make a type alias for type reuse in greater contexts? Also, this approach would require evaluating an expression or doing something weird in a macro to work blindly over the type of an expression. macro_rules! do_stuff {
($x:expr) => {
let _: ?T = $x;
let y = <?T>::new(/* ... */);
$x.do_stuff_with(y);
}
} Something important that an implementor of this feature would need to consider is macro hygiene with respect to inference types used within macros. If a
Would this be done within the compiler directly or as a clippy lint? I don't see how this feature is achievable with generics or a procedural macro. Generics erase all concrete type information that can then only be added with traits. Also, if you can't do this right now in a normal macro, how would this be achieved with a procedural macro? Both are simply forms of token substitution. |
I think the macro invocation would look like
which then create code like
It merely makes the inference variable a type parameter of some nested function. |
True, but I don’t think this is more weird than using an expression in a type. Also, macros already do some weird/awkward things like passing the types in as meta-variables. I suppose this is a personal judgement.
I propose that one wouldn’t do that (at least for now). Would it still solve most of the use case to only be able to use this inside of a function body? It seems like normal generics can cover most of the other use cases, right?
I imagine it would follow the dame rules as normal generics, right? Is there an additional challenge here?
Clippy is probably more appropriate, IMHO. |
My understanding was that the general problem of "naming the unnameable" types is supposed to be solved by Personally, I don't think we can really evaluate the usefulness of a Unless I'm missing something huge, the motivation for the RFC seems really thin. There's no example of code that's impossible today yet possible with this feature, only examples of code that would be very slightly easier to write with this feature. If the goal is indeed to make already fairly straightforward code easier to write, we should be talking about sugars, not type system extensions. If we're actually talking about code that is painfully convoluted today, yet easy with this feature, then I like @mark-i-m's idea of exposing inference variables (it's "heavier" than a mere sugar, but it's definitely not a true type system extension either), but the RFC needs some examples that clearly show the alleged pain and gain. |
Exposing inference variables would have the significant drawback of not working outside of a function – yet outside of a function is precisely where this feature would be most powerful, since within a function you can already take advantage of inference to achieve similar results in many cases. For example, the fn default_of_same_type_as<T: Default>(_: &T) -> T { T::default() } Personally, I've wished for struct Foo {
count: u32 = beu32, // big endian u32
items: Vec<u32> = repeat(count, beu32),
} to: struct Foo {
count: u32,
items: Vec<u32>,
}
impl Decodable for Foo {
fn decode(stream: &mut Stream) -> Foo {
let count: u32 = beu32.decode(stream);
let items: Vec<u32> = repeat(count, beu32).decode(stream);
Foo { count, items }
}
} (Plus maybe an Problem is, a lot of structs have many fields of simple types: struct Bar {
a: u32 = beu32,
b: u32 = beu32,
c: u32 = beu32,
d: u16 = beu16,
e: u32 = beu32,
// ...
} In this case, it can get annoying to have to effectively write each field's type twice. I'd like to allow the user to omit the type and just write struct Bar {
a: <typeof(beu32) as Decodable>::Output,
// etc...
} Of course, an alternative in this case would be to have the user represent how to decode/encode the field by writing a type, like |
I think much of this can be solved with |
We've also had the proposal for an |
|
||
As of this writing, the lang team [has come to a final | ||
decision](https://boats.gitlab.io/blog/post/await-decision-ii/) regarding the | ||
syntax for `await`, and that is to make it a postfix operation: `expr.await`. |
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.
The reason .await
was chosen is because we can chain stuff after the .await
. Similarly, .match
is an operation which takes an expression and returns an expression, allowing chaining.
There is no such advantage for a type:
&'a X
,*const X
are prefix operators[X]
,(X, Y)
are circumfix- You cannot write
foo.type::Stuff
sincefoo.type
isn't a path, so you'll need<foo.type>::Stuff
(or drastically change the grammar), making associated type another circumfix operator.
So I don't see any reason why x.type
is picked instead of typeof(x)
(or typeof x
to match impl Trait
/dyn Trait
).
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 do prefer the syntax <typeof x>::Stuff
over <x.type>::Stuff
and so I may change the proposal to use this instead. It's less visual noise in my opinion.
For the "other languages" section, you can add C++ For the purposes of Syntax-wise, I would prefer a syntax which made it really clear from the beginning of the expression that it is unevaluated; if you really need to go the unevaluated way. It's jarring to discover a function call wasn't a call only at the end. |
Just from syntax perspective: please don't put expressions where types can appear. Mixing these two in the syntax tends to backfire later (with things like turbofish and struct literal parsing difficulties). Prefixing of the expression with a keyword/sigil would disambiguate it. |
It appears the named existential type RFC #2071 being accepted makes this RFC redundant. If not, this RFC should explain the difference. You can always write roughly:
but named existential type better supports the common cases where either (a) some type constraint appear outside expressions, or (b) the expression is actually evaluated. |
You mean unevaluated "at runtime", right? Finding the type of an expression does often require constant evaluation, e.g., const fn add(x: usize, y: usize) -> usize { x + y }
pub type Foo = ([0_i32; {add(1, usize::max_value())}]).type; does require evaluating Rust is not C++, but in C++ one often needs to be careful with |
Yes, exactly. I am not yet used to compile-time evaluation in Rust. |
The existential type feature doesn’t make my use case redundant, because I would need to be able to fully access the type, rather than having allowed operations limited to some trait bound known a priori (which I don’t have). |
@rust-lang/lang discussed this in today's "Backlog Bonanza" meeting. There was definite sympathy for the goals of this RFC -- a few of us were definitely missing the ability to name types, especially the "less nameable" types like those returned by @rfcbot fcp close |
Team member @nikomatsakis has proposed to close this. The next step is review by the rest of the tagged team members: No concerns currently listed. Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! See this document for info about what commands tagged team members can give me. |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
``` | ||
|
||
Note that if the expression resolves to a long operation, the operation will | ||
_not_ be evaluated. |
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.
How does this interact with trait objects where the concrete type is not known at compile time?
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.
dyn Trait
is the type. Also dyn Trait
reveals only what Trait
permits about original type, although some traits like Any
or Error
provide downcast
or type_id
methods.
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.
is dyn Trait
considered a concrete type? I would have expected the underlying type to be. Either it returns dyn Trait
or is disallowed if the underlying type cannot be known at compile time - either way I think it deserves mention in 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've no idea if concrete means anything. dyn Trait
is an unsized type with a representation expressed in terms of pointer metadata, so vaguely like [T]
.
The final comment period, with a disposition to close, as per the review above, is now complete. As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed. The RFC is now closed. |
@nikomatsakis thoughts on reopening this? |
.type
to get expression type
This proposal adds the ability to get the type of an arbitrary expression via
expr.type
.It would work as follows:
The
type
identifier is already reserved as a keyword and so this isn't a breaking change.This is a proper RFC based off of #2704.