blog/content/posts/0001-immutable-vars-vs-cons...

338 lines
11 KiB
Markdown

+++
draft = false
date = 2022-10-06T10:30:00+05:30
slug = "immutable-vars-vs-constants-rs.md"
title = "Dfferences between Constants and Variables in Rust"
description = "No really, why do constants exist among \"immutable\" variables?"
tags = [ "rust", "educational" ]
+++
The reason why the Rust language's developers can advertise features like
_thread safety_ and _memory safety guarantee_ is because of a fundamental
design ideology of immutable variables. Immutable variables are the type of
variables where, once you assign a value, it does not change.
Rust also has constants. So you might wonder "Why does Rust have Immutable
variables _and_ Constants? Aren't they the same thing?"
In this blog post, I will explain the basics of variables and constants
in Rust, and how an immutable variable differs from a mutable variable.
## Immutable variables VS Constants
If, like me, Rust is not your first programming language, this confusion is
bound to occur sooner rather than later--"Why do either immutable variables
or constants exist in Rust?"
If you have written a simple program in Rust, you will realize that Rust has
2 types of variables. One is the type which allows changing a value, even
after it is assigned, called _mutable variables_. The second type is the one
in question, immutable variables. Unless explicitly specified, Rust assumes
a variable is immutable. Meaning, once a value is assigned to your variable,
that value will never be allowed to be changed.
Rust only has one type for constants. The one which makes sense. Immutable by
default. Once a value is assigned, it is not allowed to change.
## Type inferencing
When you declare a variable in Rust, you do it using the `let` keyword. This
is different than other low-level languages like C and C++, where you are to
explicitly specify the data-type of a variable using the appropriate keyword
like, `int`, `float`, `char`, etc.
Using the `let` keyword is necessary, but specifying a variable's data-type
is not necessary. You can either leave it to the Rust compiler (`rustc`) to
take a guess--which hardly misfires--or you can annotate the type yourself.
This guessing that `rustc` does is called "type inferencing".
Type inferencing is absent for constants. It is the job of a programmer to
provide a type for a constant that is declared.
Take a look at the following code:
Filename: const_example.rs
```rust
fn main() {
let my_var = -128;
const MY_CONST = -128;
}
```
In here, I am declaring an immutable variable (`my_var`) with the value "-128"
and a constant (`MY_CONST`) with the same value of "-128". Another similarity
is that neither of them are type annotated.
Let's try and compile this.
```bash
$ rustc const_example.rs
error: missing type for `const` item
--> const_example.rs:3:11
|
3 | const MY_CONST = -128;
| ^^^^^^^^ help: provide a type for the constant: `MY_CONST: i32`
error: aborting due to previous error
```
Hmm...
`rustc` is complaining about an error on line 3 (where we declared our
constant). The highlighted part is the name (`MY_CONST`).
The help states "provide a type for the constant". The help message also
included a "recommended/suggested type" (`i32`) but it was not applied.
Hence, one of the difference between an immutable variable and a constant in
Rust is that constants **need** type annotation. Type inferencing is **not**
applicable to constants.
## Scope of declaration
Another point of difference between an immutable variable and a constant in
Rust is its scope of declaration.
Constants can be declared globally (before/outside the `main` function).
Variables, immutable or otherwise, cannot be declared globally.
Let's see this with an example.
Filename: const_example.rs
```rust
const MY_CONST:i32 = -128;
let my_var: i32 = -128;
fn main() {
println!("{my_var} {MY_CONST}");
}
```
As you can see here, I have mostly the same code as above, but I have moved
both, the variable and the constant declaration, in the global scope.
'tis compile time!
```bash
$ rustc const_example.rs
error: expected item, found keyword `let`
--> const_example.rs:2:1
|
2 | let my_var: i32 = -128;
| ^^^ expected item
error: aborting due to previous error
```
An error :(
The way I wrote the code should give you a hint. We have an error on the
2<sup>nd</sup> line. Our constant is declared on the 1<sup>st</sup> line.
This means, `rustc` did not see any problems with a constant in the global
scope. But it does have a problem with our immutable variable if it is
declared in the global scope.
## Assignable values
Another major difference between variables (immutable or otherwise) and
constants in Rust is that a constant **cannot** have a value that can be
calculated **only at run-time**.
What do I mean by this?
Take a look at the following code:
Filename: const_example.rs
```rust
cat const_example.rs
fn main() {
let pi: f32 = 3.14;
const PI_TIMES_TWO: f32 = pi * 2;
}
```
In this code, I am declaring an immutable variable (`pi`) and assigning it the
value of "3.14". Next, I declare a constant, with the same type as `pi`,
and I assign it the value of `pi * 2`.
Shall we compile?
```bash
$ rustc const_example.rs
error[E0435]: attempt to use a non-constant value in a constant
--> const_example.rs:3:31
|
3 | const PI_TIMES_TWO: f32 = pi * 2;
| ------------------ ^^ non-constant value
| |
| help: consider using `let` instead of `const`: `let PI_TIMES_TWO`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0435`.
```
As you can see, even though `pi` is an immutable variable, we get this error.
This is because `PI_TIMES_TWO` is dependent on the value stored in `pi`
to determine its own value. This is problematic because the values of
variables are **not** evaluated at compile-time.
The value assigned to a constant **must not** be calculated/evaluated at
_run-time_.
## Compile-time vs Run-time
As I just proved, constants can not be assigned a value that will be
calculated at run-time. That must raise a question if the core reason for the
existence of immutable variables and constants must be related to the
differences in run-time and compile-time.
While I am **not** someone who has contributed to the design of the Rust
language, I am inclined to assume that this might be the reason.
You can use the value assigned to a constant during compile-time, to make
decisions _while_ the code is being compiled. This can not be done using
variables, immutable or otherwise.
Let me demonstrate this using an example.
Filename: const_example.rs
```rust
fn main() {
let arr_len_var: usize = 5;
const ARR_LEN_CONST: usize = 5;
let arr_from_const: [i32; ARR_LEN_CONST];
let arr_from_var: [i32; arr_len_var];
}
```
In this example, I am doing the following:
1. Declare an immutable variable with the value "5".
2. Declare a constant with the value "5".
3. Create an empty array, using the value of an immutable variable as the
"array size/length".
4. Create another empty array, using the value of a constant as the "array
size/length"
If, the jibberish that I just wrote above is correct, we should expect a
compilation error on the 6<sup>th</sup> line.
```bash
$ rustc const_example.rs
error[E0435]: attempt to use a non-constant value in a constant
--> const_example.rs:6:29
|
2 | let arr_len_var: usize = 5;
| --------------- help: consider using `const` instead of `let`: `const arr_len_var`
...
6 | let arr_from_var: [i32; arr_len_var];
| ^^^^^^^^^^^ non-constant value
error: aborting due to previous error
For more information about this error, try `rustc --explain E0435`.
```
As expected.
`rustc` has 2 messages for us. The first message--after looking at the
6<sup>th</sup> line--is a suggestion for us; to change `arr_len_var` from
a variable to a constant.
The second message is an error message. It is complaining that the value
which determines the length of an array, is a "non-constant value".
"But I thought the values of immutable variables could never change?! Does
that not equate to a _non-constant value_?"
You are correct, but this is something different. You see--this is where I am
applying my knowledge of what I learnt about compiler design from my
college--constants are also _evaluated_ at compile-time.
This means, the following line -
```rust
const PI: f32 = 3.14;
println!("{PI}");
```
gets replaced with the following in the first few passes of the Rust compiler.
```rust
const PI: f32 = 3.14;
println!("3.14");
```
The constant got evaluated (expanded), at compile-time. This will not be be
the case for a variable, immutable or otherwise.
This is also what happens when we use a constant to determine the length/size
of an array.
## Shadows
You might know about shadowing in Rust. It refers to the act of referring
to a different storage/address using the same name.
Below is an example of shadowing:
```rust
let five = 5;
let five = 6;
```
Here, on the first line, we are declaring an immutable variable `five`. It
assigned the value "5". In the immediate next line, we declare the variable
`five` again. This time, we assign it "6".
What this does is, when `five` was first declared and assigned the value "5",
it was given a memory address to store that "5" which we assigned to it.
Assume this memory address to be `0x01`.
Then, when we declared `five` again; this time with a different value, "6";
a different memory address was given to our variable `five`. This, new memory
address stored the value "6". Assume this memory address to be `0x02`.
Now, the older memory address (`0x01`) is not overwritten with "6", instead of
"5". It is still kept--maybe because this shadow was a local change and we
will need the previous value again? who knows--intact. But now, when we ask,
"Hey `five`, what is your assigned value?", it will check the memory location
`0x02` and give us a value from there; which is "6".
This isn't possible for constants.
- You can not shadow a constant with a constant (of any type).
- You can not shadow a constant with a variable (of any type).
- You can not shadow a variable with a constant (of any type).
## Minor nit-picks
A few minor differences between a constant and an immutable variable are as
follows:
- Variables are immutable by default, but they can also be mutable, if
asked nicely. On the contrary, constants are _always_ immutable. (You cannot
use the keyword `mut` next to the `const` keyword.)
- To declare a constant, we use the `const` keyword, but to declare an
immutable variable, we use the `let` keyword. A variable can be made
immutable if, at the time of declaration, the `mut` keyword is used alongside
the `let` keyword.
## Conclusion
The intelligent mind who were designing the Rust language were obviously not
out of their minds when they created variables that default to immutability
when constants would also exist.
To recap, constants need type annotations, but they can be declared in the
global scope; values assigned to constants cannot be something that is
calculated at run-time and they can not be shadowed.