-
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
Const/static type annotation elision #2010
Conversation
```rust | ||
const THE_ANSWER = 42; // nothing in RHS indicates this must be i16 | ||
|
||
fn get_big_number() -> i16 { |
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.
Nitpick: -> i64
.
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.
Thanks, fixed.
back in later. | ||
|
||
Fallback is acceptable, however, if the overall type is still unique even | ||
without the fallback rules, as in this example: |
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 it easy to enforce such rule? For instance
const fn foo<T, U>(_: T, u: U) -> U { u }
const A = foo(1, 2u64); // should be ok?
const B = foo(1u64, 2); // should be error?
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.
Your assumptions are correct: A
should type-check, but B
should not.
According to my understanding of Rust's type inference mechanisms, this is always enforceable. Either the unification algorithm comes up with a unique type, in which case it's okay, or it doesn't, in which case the expression results in a type error. My understanding is limited, though, so I'd be happy to learn about any counterexamples.
👎 , for several reasons:
|
@est31 as you laid this out, I see something that I think might be interesting: |
A more conservative alternative would be to allow the type to be elided only when the RHS consists entirely of literals. So for example you could write (This would presumably be true only for monomorphic literals, so for example |
@est31 Regarding functions, this proposal is more like inferring the return type of a function, but not its argument types (perhaps that's what you meant). However, constants are most often very simple things where the type is obvious, while functions tend to be much harder to mentally type-check at a glance. Generally, I expect most constants/statics will be simple expressions with an obvious type, and even the use of constant functions will likely be more for things like I agree that the complexity of when an annotation can be elided or not is problematic, and this is probably the biggest downside of the proposal. Ideally we eventually find that there's little harm in allowing the fallback rules to apply here and change the rule to always allow type elision. This proposal would be a conservative step towards that, but admittedly there's no guarantee it happens. In the meantime, I think good error messages could ease this pain for beginners. |
@glaebhoerl Leaving aside |
FWIW, we have the compiler infrastructure to determine the type of a global by type-checking its body and you get automatic cycle detection, enforcing a DAG between such global items. |
In my opinion, this should be consistent with the rules of global items always requiring annotations, but allowing inference for function-local items. Under this rule, we could allow inference for consts that are local to a function. This would also help with the ergonomics of using |
@crumblingstatue Items, placed in a function, are still quite global, e.g. you can use them in an |
@eddyb Oh, never mind then. Just curious: Is there any legitimate use case for impling an outside item inside of a function? |
It's not explicitly supported - it just results from the fact that everything that can go directly in a module also can be inside any block expression - yes, this includes |
I don't really have an opinion. My comment was just like "if we think that's a problem, then another option is..." |
* Const functions may make it more difficult for a programmer to infer the type | ||
of a const/static item just be reading it. Most likely, though, most uses of | ||
const functions in this context will be things like `AtomicUsize::new(0)` | ||
where the type is obvious. |
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.
Why is this most likely? Once there are fully fledged const functions they may be used for all sort of things.
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.
True, they can be used in lots of ways, but since the motivation for that RFC was to initialize types like AtomicUsize<T>
and Cell<T>
in constant contexts, I'm assuming that will be the typical use-case. I could be wrong, though; perhaps people who were involved in that RFC discussion could provide more info.
In those more complicated cases where documenting the type is important for readability, the programmer should leave the type in. But mandating that practice by requiring annotations on all const
/static
items, even the simple ones, seems like overkill to me. I'll add a note about this to the RFC.
Sure. e.g. if a function wants to do a visitor pass, then we can create the visitor and its impls inside that function instead of in the module, e.g. this: https://github.com/rust-lang/rust/blob/master/src/librustc_mir/transform/mod.rs#L74 - this is more useful if your functions are impl-items, so you would have to add the struct outside of the impl. |
Thanks @schuster for the RFC, and thanks to those who've commented so far! It seems opinions thusfar have been mixed. Let me start by covering what are, in my mind, the most important drawbacks raised so far.
I think the above are the most significant drawbacks, so I'll stop there for the sake of keeping debate focused. On the positive side, to me a pain point is the need for inelegant, noisy stuttering: static COUNT: AtomicUsize = AtomicUsize::new(0);
const USER_VISIBLE: Flags = Flags { /* ... */ }; In cases like these, readability is helped by leaving off type information, because it's easier to quickly see the relevant information and not have to skip over stuttering. I'd love to see examples from real code where it would be possible to leave off a type annotation, and that would significant impair the reader's ability to determine the type. |
@aturon I think one potentially-scary-seeming drawback is that you might modify one part of your public API, and accidentally end up changing another part of it as well, because the type of the second was inferred from the first. I honestly don't have any idea whether this would be a thing that actually happens in practice. Has anyone had any experience where they changed the type of a thing in their API, got a type error on a For that matter, it could happen cross-crate as well. |
Wouldn't that "unexpected" change be what you'd intend to do anyway? Accidental semver-incompatible changes on constants are a very unlikely problem that can be discovered by automated tools. As much as I'm against giving programmers more decisions, in this case I support elision. You can let the author decide whether they want the type inferred or not, same as with locals. |
You can still teach this in parallel with eliding type annotations for |
That's what my question ("Has anyone had any experience where ...") was about. |
@glaebhoerl To make this concrete, you're proposing a scenario like the following, right? Somewhere in a crate, we have: const FOO: i32 = 5; and elsewhere (in this crate or in another one that imports the first) we have: const BAR = (FOO); // inferred type is (i32) Then if someone changes the annotation on I agree with @le-jzr that this seems unlikely in the cross-crate scenario, which seems like the more dangerous of the two (because the person who makes the change and the person who notices a problem are not the same). But like you, I'm also curious if anyone has had the kind of experience you're asking about. |
cc @rust-lang/lang, it'd be good to start getting some additional thoughts on the tradeoffs here. I've written up a summary with my own thoughts about some of the arguments. |
To my mind, you hit the nail on the head with this observation:
In particular, I've been trying to put my finger on why this RFC feels like such a win to me, whereas eliding return types on functions does not. I think it is precisely a question of the analogy with I also frequently find that I am making constants that are not "top-level constants" but rather constants within a function. In that context, they are technically nested top-level items, but my brain does not process them that way: fn process_items(vec: &[i32]) {
const CHUNK_SIZE = 32; // requiring `: usize = 32` doesn't feel like a win here
for chunk in i.iter().windows(CHUNK_SIZE) { ... }
} On the other hand, this example raises an interesting point. =) In particular, that bit of code would not work with this RFC, since I believe it would require a type annotation and, even if it didn't, it would presumably infer to Regardless, there are other examples (e.g., string constants, or more complex constants) that fit the mold. |
I've checked my code,
|
I think the defaults are weakly documented, and having to know them wouldn't be nice from a learnability perspective. I think its harder to remember what the defaults are than remembering that you have to put type annotations on number literals. |
If RFC 2071 "Add impl Trait type alias and variable declarations" ends up with the less conservative alternative (underlying type of $vis const C = init_expr; could be then desugared into // EDIT: `Anything` is not part of the desugaring, it lives somewhere in the standard library.
trait Anything {}
impl<T> Anything for T {}
// Desugaring
$vis const C: impl Anything = init_expr; If cc @cramertj |
@petrochenkov Which part of either RFC would allow the creation of a new alias like that? This one would only allow type elision if a unique type can be inferred locally, whereas many types could be inferred for your example (e.g. So if both RFCs were accepted and implemented, I would expect your example (before desugaring) to result in a compiler error. I wouldn't expect the compiler to generate both a new trait and a new |
It doesn't, but it provides all the underlying mechanisms. Adding type elision through desugaring into
Right, |
When reading code, the TYPE matters more than VALUE. The later does not help a lot to understanding code. If TYPE is elided, I (the reader, the programmer) must infer it myself, it's not obvious in some cases, and the compiler can't help me here (though rustc will infer it too). Update: for local vars, we have context. |
A question from me: Would this RFC potentially allow the following?
I'm not sure if I'd be in favor of or against this being permitted. On the one hand, it makes perfect sense syntactically - if you don't need to name the type an unnameable type is no problem, and the closure would syntactically only be able to capture On the other hand, it would enable a much more terse syntax for declaring functions, one which I am not especially in favor of; see my objections to a request for such a syntax. |
nice idea!!! I would be greatly in favour of allowing this; it would allow easy migration of code between lambdas and functions. For another take on this , see the path being pursued in the JAI language, his idea is to make evolving code easy ('code goes through a maturation cycle'). (tangentially, Kotlin also has a nice idea ) |
@rfcbot fcp postpone I'm moving to postpone the RFC. Overall, while I still think that there's a problem here to be solved, I'm not sure that we've quite found the right formula for doing it. I think we should put this off and return to it in the future, once some of the dust has settled. One point that sticks out in my mind is that, based on the data that @schuster gathered, it looks like enabling On the other hand, there are two other non-trivial cases that would be helped:
The alternatives I see:
|
Team member @nikomatsakis has proposed to postpone this. The next step is review by the rest of the tagged teams: No concerns currently listed. Once these reviewers reach consensus, 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. 🔔 |
1 similar comment
🔔 This is now entering its final comment period, as per the review above. 🔔 |
For anyone who tries to tackle this in the future, here are the two main sticking points I see:
|
The final comment period is now complete. |
This RFC has been closed as postponed. While the lang team believes that there is room for improvement here, it's proven to be a much more complicated design space than hoped, and the payoff doesn't appear worth the complexity at this time. Thanks, @schuster, for writing and shepherding the RFC! |
What's the current status on this? |
@alexreg I don't believe anything has changed here since the FCP proposal and sticking points comments above. That said, I wouldn't be surprised if there were a few cases of narrower proposals that could be interesting, like how eliding |
I've just realized that #2010 (comment) ( use std::any::Any;
const FOO: &Any = &|a, b| a + b; error[E0282]: type annotations needed
--> src/lib.rs:2:27
|
2 | const FOO: &Any = &|a, b| a + b;
| ^ cannot infer type Since this feature alone, without additional global inference, would rely on type-checking the body to get its overall type, it would hit the same problem of not being able to infer the closure's signature. OTOH, something like const FOO: &Any = &|a| Option::unwrap_or(a, 0); So if we're worried about the first example but not the last one, then I think it wouldn't be a mistake to accept this feature. If we're worried about the last one too, we can't do much, because this works: #![feature(fn_traits, unboxed_closures)]
use std::any::Any;
use std::marker::PhantomData;
trait CallAny {
fn call_any(&self, args: Box<Any>) -> Box<Any>;
}
// HACK: force dispatch to use M, not just T.
struct Mark<T, M>(T, PhantomData<M>);
impl<A: 'static, R: 'static, F: Fn<A, Output=R>> CallAny for Mark<F, A> {
fn call_any(&self, args: Box<Any>) -> Box<Any> {
Box::new(self.0.call(*args.downcast::<A>().unwrap())) as Box<Any>
}
}
const FOO: &CallAny = &Mark(|a| Option::unwrap_or(a, 0), PhantomData);
fn main() {
println!("{:?}", (
FOO.call_any(Box::new((None::<i32>,))).downcast::<i32>(),
FOO.call_any(Box::new((Some(123),))).downcast::<i32>(),
));
}
|
A proposal to allow elision of type annotations in many cases for
const
andstatic
items, as part of the ergonomics initiativeRendered