- Feature Name: const-static-type-elision
- Start Date: 2017-04-29
- RFC PR:
- Rust Issue:
Allow type annotations to be elided on all const and static items with a unique type, including in traits/impls.
In most cases, the type for constant and static items is obvious, and requiring a redundant type annotation is a papercut many programmers would want to avoid. For example, these two declarations would result in compiler errors in the current version of Rust:
const GREETING = "Hello, world!"; // unique type: &'static str
static NUM = 42i32; // unique type: i32
This is usually no more than a small annoyance, but the risk involved in eliding the types also seems small.
In the terms of the ergonomics initiative blog post, this change is broadly applicable, but the power is restrained by the limitations on type inference described below.
Rust would allow const
and static
items to elide type annotations and infer
the type, but only if type inference can infer a unique type for the expression
before applying any fallback rules. So if we have the following items:
struct S {
a: i32
}
const A: bool = true;
const B: i32 = 42i32;
const C: &str = "hello";
const D: S = S { a: 1 };
const E: [f32; 2] = [1.0f32, 2.5f32];
They could be written like this:
struct S {
a: i32
}
const A = true;
const B = 42i32;
const C = "hello";
const D = S { a: 1 };
const E = [1.0f32, 2.5f32];
To minimize the reasoning footprint, type elision would use only local type
inference, rather than attempting to infer a type based on a later use of the
item as with let
-bound variables. For example, the following would result in a
type error, because there are multiple possible types for the literal 42
(e.g. i16
, i32
, etc.), even though the use in get_big_number
would require
it to be i64
.
const THE_ANSWER = 42; // nothing in RHS indicates this must be i64
fn get_big_number() -> i64 {
THE_ANSWER
}
The fallback rules (specifically, defaulting integer literals to i32
and float
literals to f64
) are disallowed in cases where multiple typings are valid to
prevent the type of an exported item from changing only by removing a type
annotation. For example, say some crate exports the following:
const X: i64 = 5;
If the developer later decides to elide the type annotation, then fallback would
infer the type of X
as i32
rather than i64
. If X
is exported but not
used within the crate, then this change could break downstream code without the
crate developer realizing it. Admittedly, that scenario is unlikely, but
ruling out fallback is the most conservative option and could always be added
back in later.
Fallback is acceptable, however, if the overall type is still unique even without the fallback rules, as in this example:
const fn foo<T>(_: T) -> char { 'a' }
const X = foo(22);
This design would allow closures (rather than just references to closures) to be
used as const
/static
items, because the programmer no longer has to write
down an inexpressible type. This shouldn't pose any particular difficulties from
an implementation perspective, but it's worth being aware of.
Documentation projects such as rustdoc may need to deal with this as a special case. @withoutboats suggests coercing closures to fn types as one possible solution.
The Rust Reference should record the rules for when the annotation is
optional. The Rust Programming Language and Rust by Example should remove
the sections that say annotations are required, and they may want to consider
removing annotations from their examples of const
and static
items (see
"Unresolved questions" below).
- Some users may find it more difficult to understand large constant expressions
without a type annotation. Better IDE support for inferred types would help
mitigate this issue, and a "best practice" of annotating the types on
particularly complicated items where documentation is important should be
encouraged.
- Const functions in particular may make manually inferring a type
difficult. Given
the original motivation for const functions,
though, most uses in this context will likely be things like
AtomicUsize::new(0)
where the type is obvious.
- Const functions in particular may make manually inferring a type
difficult. Given
the original motivation for const functions,
though, most uses in this context will likely be things like
- Allow numeric literals in const/static items to fall back to
i32
orf64
if they are unconstrained after type inference for the whole expression, as is done with normallet
assignments. If the constant is visible outside its crate but not used within the crate, this could change the constant's type without any warning from the compiler. That case is likely rare, though, and experienced Rust programmers would likely expect this kind of fallback, especially for simple cases likeconst A = 42;
.
- Should The Rust Programming Language remove the annotations used when introducing constants?