Why is type declaration necessary in constants?

Hello, Rust newbie and enthusiast here (I have some background in C/C++). Really happy to be exploring this beautiful language!

Anyway, on to the question. I was wondering why we are required to specify type and size when declaring constants? I mean, the compiler is smart enough to figure it out in let declarations; why can't it do so for const?

Many thanks in advance!

2 Likes

I can think of a couple reasons. For one, what would you expect the type of x to be here?

const x = 42;

Keep in mind that the compiler doesn't automatically coerce between numeric types like C/C++, so a function which takes an i32 isn't compatible with a u64.

Another thing to think about is that static and const variables are usually part of your API, and Rust prefers to keep things explicit so you just need to look at the definition of a static variable or a function (e.g. the fn foo(n: u32) -> u32 part) and that's all you need to know, something often called "local reasoning".

If you require explicit type signatures in anything which could become part of your library's API then we've found that to be a pretty good middle ground between explicitness/readability and convenience.

4 Likes

I apologize in advance if my arguments are going to sound naive, and I still haven't covered stuff like functions, etc.

I'd like to answer as follows; the same type I would expect from:
let x = 42;

I can agree with the local reasoning part, though. If public-facing APIs are not unambiguous, it can nothing but disaster.

So, can we conclude by saying that the Rust compiler can infer types of constants, but chooses not to, in the favor of local reasoning?

1 Like

When you write let x = 42, the compiler infers the type of that integer from the way in which it is subsequently used. If the code expects an u8, that will be an u8. If the code expects an u32, that will be an u32. If there are multiple code paths with diverging expectations, the compiler will be unhappy and cry out an error message.

This is appropriate for variables which are declared locally and never exit their host function. But for public globally scoped data, it makes the type of the value sensitive to the implementation of the module, or of its users, which is a recipe for maintenance problems.

You can compare this approach with the one taken by C or Java where every value has a well-defined type (IIRC int for integers and double for floating-point data, overridable with a suffix). It has different tradeoffs: it arguably works better in this case, but less well in others (causing unexpected integer truncation or unnecessary double-precision computation for example).

A possible middle ground would have been for Rust not to require type annotations when the type of a value is unambiguous, as in "0i32" or "4.2f64". I can only guess that this one was decided against because it added one more language rule for what is ultimately a small reduction in verbosity.

1 Like

To add to what @HadrienG said, when you use let x = 42, the compiler can see every use of x in order to determined what its type must be. In contrast, when you declare a const x = 42, the compiler has a much more challenging time identifying every use, particularly if it is public.

Another reason is to provide documentation for humans. Even in Haskell (where global inference is possible and used) it is best practice to document all top-level types, because otherwise a human has to read and understand the entire program in order to determine the type of a given function (or constant). Mistakes (with global type inference) end up leading to confusing error messages, since the compiler can't identify where the type error is wrong, which end up leading you to specify the types anyhow.

4 Likes

I think this improvement will eventually be accepted in Rust. Many times there's no type ambiguity when you define a const. Code noise reduction helps.

Edit: there's another case where we could reduce the noise:

fn foo(MyBarStruct { x, y }: MyBarStruct) {}

=>

fn foo(MyBarStruct { x, y }: _) {}
1 Like

I'm sorry but I feel lost here. How does something like const x = 42; give the compiler a hard time? It's a constant that's never going to change, either in value or in type. In the odd case you end up doing something like x = 23; the compiler will immediately complain.

The way I see it, in a compiled language, the compiler can always see every use of the constant across the program . . . ?

Yup, I feel so, too. Thanks for summarizing this neatly! :slight_smile:

Thank you. Once again, maintenance is the only angle I can understand/accept so far. I think Golang has a similar treatment of variables . . . but I digress.

What is its type? It isn't specified. And while it's true that for a private const the compiler can see all uses, that is not true of a pub type.

While the compiler is perfectly capable to infer the constant's type based on global usage analysis (like in the case of local vars) the capability is turned off in order to follow the principle of least astonishment. Which global usage sites would take the preference in type inference? The ones directly below the declaration? What if the user would write a valid function using the constant above the previous top usage site but resulting in different type being inferred? All the other (perfectly valid) usage sites would start spewing compile errors. And what would such inference precedence even mean if all the usage sites are in different module?

Numbers are a bit of a special case anyway, because a literal's type is flexible - there isn't really anything else like that in Rust.

There was a tiny step in the direction you're suggesting - string constants are now always assumed to be 'static rather than requiring it to be explicitly specified, basically because there are no other options.

I could imagine a outcome where:

  • numeric constants always need some kind of explicit type annotation
  • non-public constants can have inferred types, unless there's any ambiguity
  • public constants always have explicit types because they're part of an API

This might all get more complex with const fn, so it may be we'll need to have that stabilize before loosening the types around constants.

It may seem a good idea to allow const X = 1u32; because the supplied value's type is unambiguous. But what about other cases where the type is also unambiguous but not as obvious? I don't know about the status of const expressions in Rust, but I think at least eventually we will be able to do const X: SomeType = someConstFunction();. The return type of the function can be unambiguous, but it's not visible at the place of the call unless the constant has explicit type.

I think the rules should depend on the visibility of the constant. If the constant is only available within its module, Rust can allow implicit type (or even type inference in ambiguous cases) because it's really not much different from let. If the constant is visible outside of the module, it must always be declared with a type.

1 Like

Notably, the summary comment:

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.

2 Likes

Very interesting. I wonder why that is. I thought that was the whole idea of a compiler . . . it can parse all the project files beforehand and know in advance the types of all variables and constants and therefore catch error easily.

Wow, thanks for the link. I'm intrigued by this statement from the discussion:

It would make the language harder to learn as you now need to know when you can omit the type and when not. The current rule is strict and simple. Yet another scary area for beginners to not know about!

I'd like to read more about type omissions in Rust; perhaps that can answer my question at a fundamental level. Any resource/tutorial you suggest?

Thanks, @jsgf. Are you from the core Rust team? :slight_smile:

My vote goes to uniformity in the language, especially in a systems programming language. At some level I'm okay with the present arrangement as well, but it should be added to the docs and the book to make it more clear.

I'm sorry to bring it around full circle, but in my opinion, what applies to local variables applies to constants, as far as compiler capability is concerned. If the compiler can catch conflicting inference for local variables, it can for constants as well. And if it tries to make things easy by being easy on local variables, it should for constants, too.

The only point I can agree on so far is maintenance; because constants are public, not specifying a type can result in a maintenance nightmare.

Only when compiling an executable can a compiler see all the uses of functions and constants. When compiling a library crate, the compiler needs to ensure that the code will compile when linked into any other crate that conforms to its API. Inferring that API seems like a poor idea, as it could easily lead to accidental changing of the API.

1 Like

Constants are by their very nature very non-local. They can be public, which is something local variables cannot be. So the compiler doesn't always have the full overview of the code when it's compiling your crate.

You might say that the compiler should look at how the constants are being used, but that could mean that the constants would change type depending on how the dependent crates use your crate?

Go handles this by letting constants be untyped, but Rust doesn't have that concept.