I understand const, mut & shadowing, but why does it exist?

I do understand the technical facts, I think. But what I really don’t understand (and I haven’t found an explanation for my beginners mind yet):

why would I use a const if a let does about the same?

what problem is solved by shadowing?

It's particularly important for things like

if let Some(foo) = foo {

or

let x: i32 = x.parse()?;

where it's changing the type, but not really changing the intent.

Without shadowing, you end up with unfortunate hungarian things like

if let Some(foo_really) = foo {

or

let x: i32 = x_text.parse()?;

It's also really handy in #[test]s.

11 Likes

const binds a name for a compile time constant, which means you can use it where only compile time constants are expected. This includes for example initializers of static variables, arrays lengths, non-copy initializers of by-length array literals, const generics and more.

let instead binds a name for any runtime value, so you can't do any of the above with it.

1 Like

Please explain if you will, why then would one ever want to use let, if what you really want is a const or a let mut?

So is it more a thing of beauty than a thing of functionality?

And I do understand beauty is functional in a language!

Why would it be the case that all you ever want is a const or a let mut? This is a false dichotomy. Sometimes you need a run-time value which you don't want to change subsequently. It can't be a const (because it's not a value known at compile-time), but it can't be a let mut either (because you don't want to allow changing it).

3 Likes

There are operations that cannot be done at compile time that you none the less need to store in a variable, even when you don't need the capabilites a mut binding confers.

That said, non-mut bindings are effectively a lint (albeit one some people feel quite strongly about).

2 Likes

I see, thanks, the penny dropped!

I’d suggest, you try actually using these things in some programs and let the compiler tell you what const and let can or cannot do.

The meaning of let mut is probably unsurprising…

fn my_function(n: u8) {
    let mut x = n + 10;
    x += 1;
}

but if we don’t mutate it anymore

fn my_function(n: u8) {
    let mut x = n + 10;
}
warning: variable does not need to be mutable
 --> src/lib.rs:2:9
  |
2 |     let mut x = n + 10;
  |         ----^
  |         |
  |         help: remove this `mut`
  |
  = note: `#[warn(unused_mut)]` on by default

The compiler suggests to remove the “mut” and use let x = …, which will work fine. But let’s writing “const” instead, if we don’t really know what let without mut is there for.

fn my_function(n: u8) {
    const x = n + 10;
}
error[E0435]: attempt to use a non-constant value in a constant
 --> src/lib.rs:2:15
  |
2 |     const x = n + 10;
  |     -------   ^ non-constant value
  |     |
  |     help: consider using `let` instead of `const`: `let x`

error: missing type for `const` item
 --> src/lib.rs:2:12
  |
2 |     const x = n + 10;
  |            ^ help: provide a type for the item: `: <type>`

Well, this didn’t work at all! I mean, let’s look at the errors individually. For once, it suddenly wants a type annotation

fn my_function(n: u8) {
    const x: u8 = n + 10;
}

but even then no luck

error[E0435]: attempt to use a non-constant value in a constant
 --> src/lib.rs:2:19
  |
2 |     const x: u8 = n + 10;
  |     -------       ^ non-constant value
  |     |
  |     help: consider using `let` instead of `const`: `let x`

The compiler even kindly suggests moving back to a let.

When does const work? The const’s value must be independent from anything that happens at run-time, including function inputs, or evaluating functions that compile-time-computation features of Rust do not (or not yet) support, like allocations, randomness, etc…

So this works

fn my_function(n: u8) {
    const x: u8 = 42 + 10;
}

but it gives another warning

warning: constant `x` should have an upper case name
 --> src/lib.rs:2:11
  |
2 |     const x: u8 = 10 + 10;
  |           ^ help: convert the identifier to upper case (notice the capitalization): `X`
  |
  = note: `#[warn(non_upper_case_globals)]` on by default

So apparently consts are stylistically very different from let; they ought to have UPPER_CASE names, and are required to have a type signature. What else? The compiler doesn’t hint at this in this case, but consts declarations are items not statements, which is a fancy way of saying, you can write them in a lot more places than just function bodies (or blocks), in particular at the top level of the file

const X: u8 = 10 + 10;

fn my_function(n: u8) {}

Can’t do that with let:

let X: u8 = 10 + 10;

fn my_function(n: u8) {}
error: expected item, found keyword `let`
 --> src/lib.rs:1:1
  |
1 | let X: u8 = 10 + 10;
  | ^^^ consider using `const` or `static` instead of `let` for global variables

Because they are items, just like it’s the case for functions, Rust took the decision to make type signatures on consts mandatory. And the UPPER_CASE naming scheme is important to not accidentally confuse them with local variables, which is especially important since you can even pattern-match against the value of a constant[1].

Now, looking at the last compiler suggestion above… what is static?! I’d say that’s where the information the compiler can teach us straightforwardly ends, as the difference between const and static is a bit more subtle. I do go into it in the bottom part of this answer (including code examples with some compilation errors, and behavioral differences), but I’d say it’d be hard to understand the differences here by experimentation, without any guidance.

One more thing that const allows us, and one of the most relevant things actually, is that it can be used in places where constants are required. A typical example are array lengths.

fn my_function(n: u8) {
    let x: [i32; 42] = todo!(); // works fine
    let y: [i32; 40 + 2] = todo!(); // works fine
    let y: [i32; n] = todo!(); // doesn't work
}
error[E0435]: attempt to use a non-constant value in a constant
 --> src/lib.rs:4:18
  |
1 | fn my_function(n: u8) {
  |                - this would need to be a `const`
...
4 |     let y: [i32; n] = todo!(); // doesn't work
  |                  ^

That using the input parameter as an array length won't work is a fundamental limitation that cannot be worked around using arrays on the stack. (One would want to use Vec<i32> or perhaps Box<[i32]> in this case.)

But what if we want to give the length a name, e.g. to not repeat ourselves when it’s used a lot?

fn my_function() {
    let n = 42;
    let x: [i32; n] = todo!(); // length n
    let y: [i32; n] = todo!(); // same length, DRY
}
error[E0435]: attempt to use a non-constant value in a constant
 --> src/lib.rs:3:18
  |
2 |     let n = 42;
  |     ----- help: consider using `const` instead of `let`: `const n`
3 |     let x: [i32; n] = todo!(); // length n
  |                  ^ non-constant value

error[E0435]: attempt to use a non-constant value in a constant
 --> src/lib.rs:4:18
  |
2 |     let n = 42;
  |     ----- help: consider using `const` instead of `let`: `const n`
3 |     let x: [i32; n] = todo!(); // length n
4 |     let y: [i32; n] = todo!(); // same length, DRY
  |                  ^ non-constant value

The compiler suggestions are – once again – perfectly helpful. In this case, a let won’t work, and a const is required!

fn my_function() {
    const N: usize = 42;
    let x: [i32; N] = todo!(); // length N
    let y: [i32; N] = todo!(); // same length, DRY
}

To also get into that topic really quickly, let’s do a very short comparison with static, too.

static are items like const, in UPPER_CASE, with a type signature and you can write them on top level. So far so similar

const N: usize = 42;
static M: usize = 100;
fn main() { println!("{}", N + M) }

If you use both, you might notice that statics can be defined using consts

const N: usize = 42;
static M: usize = 100;

static SUM: usize = N + M;

fn main() { println!("{}", SUM) }

but not vice-versa

const N: usize = 42;
static M: usize = 100;

const SUM: usize = N + M;

fn main() { println!("{}", SUM) }
error[E0013]: constants cannot refer to statics
 --> src/main.rs:4:24
  |
4 | const SUM: usize = N + M;
  |                        ^
  |
  = help: consider extracting the value of the `static` to a `const`, and referring to that

Also, statics cannot be used in places like array lengths where consts are required:

const N: usize = 3;
static M: usize = 3;

fn main() {
    let array: [i32; N] = [1, 2, 3]; // fine
    let array: [i32; M] = [1, 2, 3]; // not okay
}
error[E0013]: constants cannot refer to statics
 --> src/main.rs:6:22
  |
6 |     let array: [i32; M] = [1, 2, 3]; // not okay
  |                      ^
  |
  = help: consider extracting the value of the `static` to a `const`, and referring to that

So the first conclusion is that consts are more useful, if you really have a constant value, because you can then use them for other consts and for array lengths and the like, so prefer using const if possible. Now, when are consts not usable? That’s when you truly need a physically present global variable, in particular when the value in the variable must be mutable, using things like a Mutex in std::sync - Rust, or OnceLock in std::sync - Rust, or atomics, etc…

(another use case for static instead of const can - occasionally - be that statics are guaranteed to exist as one global value in one singular place, so they can sometimes be more reliably efficient when using a const that contains a huge (in size) constant like a super large array, would create too much overhead in case this data was ever duplicated into different places)

use std::sync::Mutex;

static GLOBAL: Mutex<i32> = Mutex::new(0);
fn incr() {
    *GLOBAL.lock().unwrap() += 1;
}
fn read() -> i32 {
    *GLOBAL.lock().unwrap()
}
fn main() {
    incr();
    incr();
    incr();
    println!("{}", read()); // -> 3
    incr();
    println!("{}", read()); // -> 4
}

This won’t work with a const, as those are, unsurprisingly, not usable for non-constant values :wink: Let’s try nonetheless

use std::sync::Mutex;

const GLOBAL: Mutex<i32> = Mutex::new(0);
fn incr() {
    *GLOBAL.lock().unwrap() += 1;
}
fn read() -> i32 {
    *GLOBAL.lock().unwrap()
}
fn main() {
    incr();
    incr();
    incr();
    println!("{}", read());
    incr();
    println!("{}", read());
}

compiles fine… outputs

0
0

No warnings :frowning: – yeah, the fact that this still works is a bit unfortunate, arguably we should get a warning here, too. At least clippy helps though:

warning: a `const` item should never be interior mutable
 --> src/main.rs:3:1
  |
3 | const GLOBAL: Mutex<i32> = Mutex::new(0);
  | -----^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  | |
  | make this a static item (maybe with lazy_static)
  |
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#declare_interior_mutable_const
  = note: `#[warn(clippy::declare_interior_mutable_const)]` on by default

warning: a `const` item with interior mutability should not be borrowed
 --> src/main.rs:5:6
  |
5 |     *GLOBAL.lock().unwrap() += 1;
  |      ^^^^^^
  |
  = help: assign this const to a local or static variable, and use the variable here
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#borrow_interior_mutable_const
  = note: `#[warn(clippy::borrow_interior_mutable_const)]` on by default

warning: a `const` item with interior mutability should not be borrowed
 --> src/main.rs:8:6
  |
8 |     *GLOBAL.lock().unwrap()
  |      ^^^^^^
  |
  = help: assign this const to a local or static variable, and use the variable here
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#borrow_interior_mutable_const

Why this code still works is because mentioning consts treats them like values, not like places (e.g. like a variable). So writing &MY_CONSTANT will not create a reference to some global MY_CONSTANT variable – as it would if MY_CONSTANT was a static instead – but instead it initializes a temporary variable with the contents copied from MY_CONSTANT and creates a reference to that variable. Like writing &42 works, or in the above example if *GLOBAL.lock() was re-written into *Mutex::new(0).lock() on both occurrences – which also explains the behavior of always observing the 0 in the read() function.


  1. For example:

    const N: u8 = 42;
    fn test(x: u8) {
        match x {
            N => println!("it’s 42!"),
            n => println!("It’s not 42, but {n}!"),
        }
    }
    
    ↩︎
14 Likes

This is great, thanks a lot!

1 Like

I recently used const and static like this:

#[cfg_attr(unix, path = "gui_gtk.rs")]
#[cfg_attr(windows, path = "gui_win.rs")]
mod gui_impl;
use gui_impl::INTERFACE_CONST;

/// [`Lazy`] static holding the [`Interface`] to the GUI thread
///
/// Initialization is performed platform dependently, see
/// [`gui_impl::INTERFACE_CONST`].
static INTERFACE: Lazy<Interface> = INTERFACE_CONST;

And then in the submodule:

/// Implementation for [`super::INTERFACE`]
#[allow(clippy::declare_interior_mutable_const)]
pub(super) const INTERFACE_CONST: Lazy<Interface> = Lazy::new(|| {
    /* … */
});

Note how I had to shut up clippy :paperclip: :wink:

(see also LazyLock in Nightly)

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.