Skip to content
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

Closed
wants to merge 3 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions text/0000-const-static-type-elision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
- Feature Name: const-static-type-elision
- Start Date: 2017-04-29
- RFC PR:
- Rust Issue:

# Summary
[summary]: #summary

Allow type annotations to be elided on all const and static items with a unique
type, including in traits/impls.

# Motivation
[motivation]: #motivation

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:

```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](https://blog.rust-lang.org/2017/03/02/lang-ergonomics.html),
this change is broadly applicable, but the power is restrained by the
limitations on type inference described below.

# Detailed design
[design]: #detailed-design

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:

```rust
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:
```rust
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`.

```rust
const THE_ANSWER = 42; // nothing in RHS indicates this must be i64

fn get_big_number() -> i64 {
THE_ANSWER
}
```

## Integer/Float Fallback

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:

```rust
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:
Copy link
Member

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?

Copy link
Author

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.


```rust
const fn foo<T>(_: T) -> char { 'a' }
const X = foo(22);
```

## Closures

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](https://internals.rust-lang.org/t/pre-rfc-elide-type-annotations-from-const-and-static-items/5175/2?u=jschuster)
coercing closures to fn types as one possible solution.

# How We Teach This
[how-we-teach-this]: #how-we-teach-this

_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).

# Drawbacks
[drawbacks]: #drawbacks

* 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](https://github.com/rust-lang/rfcs/blob/master/text/0911-const-fn.md),
though, most uses in this context will likely be things like
`AtomicUsize::new(0)` where the type is obvious.

# Alternatives
[alternatives]: #alternatives

* Allow numeric literals in const/static items to fall back to `i32` or `f64` if
they are unconstrained after type inference for the whole expression, as is
done with normal `let` 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 like `const A = 42;`.

# Unresolved questions
[unresolved]: #unresolved-questions

* Should _The Rust Programming Language_ remove the annotations used when
introducing constants?