23
$\begingroup$

Some programming languages (Rust is one I can think of off the top of my head) provides mechanisms to have immutable variables but they also have constants. Isn't an immutable variable just a constant? What advantage is there in supporting both?

$\endgroup$
6
  • 2
    $\begingroup$ Maybe provide some reasoning on why an immutable variable is a constant? You might be able to find some differences. $\endgroup$
    – Redz
    Commented Nov 19, 2023 at 6:46
  • 9
    $\begingroup$ Is this question about Rust, or about immutabile and constant variables in general? Rust uses very specific meanings of both of these terms that are not universal, and an answer that addresses either Rust or the broader case isn’t going to do a good job of the other (in fact, likely be contradictory!). How general is this question intending to be? $\endgroup$
    – Michael Homer
    Commented Nov 19, 2023 at 7:26
  • 1
    $\begingroup$ I decided to remove the "rust" tag since this question isn't specifically about Rust and I was just using that as one example. $\endgroup$
    – QAH
    Commented Nov 19, 2023 at 14:40
  • 4
    $\begingroup$ @QAH But in a different language those terms may have different meanings, so it’s no longer clear what you are asking about. $\endgroup$ Commented Nov 20, 2023 at 11:59
  • 5
    $\begingroup$ Immutable local variables in a method often have different values in each invocation; personally I wouldn’t call that “constant”. And then there are read-only values that I cannot modify, but someone else can. $\endgroup$
    – gnasher729
    Commented Nov 20, 2023 at 19:34

7 Answers 7

32
$\begingroup$

In many programming languages, a constant refers exclusively to compile-time constants. C, C++, and JavaScript do not appear to make this distinction and instead use the keyword for immutable bindings, but it appears in Rust, Nim, C#, Go, Kotlin, Lean...

So yes, a run-time constant is just an immutable variable. And no, a compile-time constant is not the same as an immutable variable. While distinctions between mutable and immutable variables are quite important for both compilers and programmers, a compile-time constant has a fixed value in memory, known to the compiler, and so many more optimizations may be done with it.

Terminology issues abound in computer science and I do not doubt that many other languages I've missed refer to immutable variables as constants, but it does appear broadly much more common for them to refer to values known at compile-time. See also: the idea of constexpr (C++), const exprs (Rust), comptime (Zig), static (Nim)... also, quite funny to see C++ pick up constexpr for compile-time evaluated expressions, in contrast to const's behavior in the language.

$\endgroup$
15
  • 8
    $\begingroup$ C & C++ certainly do have compile-time constants, but they don't mark them by using const; rather they mark them using enum or #define or &static_variable_name or &function_name. (Instead, const denotes immutability.) $\endgroup$ Commented Nov 20, 2023 at 4:17
  • 2
    $\begingroup$ @PasserBy Since they are just text copied into the code they lose type information $\endgroup$ Commented Nov 20, 2023 at 12:37
  • 1
    $\begingroup$ @mousetail It's not. If you did that, PI is an int. $\endgroup$
    – Passer By
    Commented Nov 20, 2023 at 15:05
  • 2
    $\begingroup$ @EthanB. That won't compile, so no, it's not. $\endgroup$
    – Passer By
    Commented Nov 21, 2023 at 4:26
  • 3
    $\begingroup$ @MartinKealey: You're right for C, but C++ has constexpr as well. That is a true compile-time typed constant (has to be, because C++ templates are compile time, and those accept constexpr arguments). $\endgroup$
    – MSalters
    Commented Nov 21, 2023 at 10:52
21
$\begingroup$

A constant is a variable whose value cannot change at all, whereas an immutable variable is a variable whose value cannot change for the lifetime of the variable, i.e. while the variable is in scope, but the value can be different between different lifetimes.

Consider the following Rust example:

const FOO: usize = 5;

fn bar(x: usize) -> usize {
    let y = x + 5;
    y * 2
}

fn main() {
    println!("{}", FOO);
    println!("{}", bar(6));
    println!("{}", bar(7));
}

In this example, FOO is a constant, since its value can never change at any point during program execution. On the other hand, y is an immutable local variable, whose value cannot change within a single activation of the function bar. But y can have different values at different times ─ in this program, it takes the values 11 and 12. Likewise, x is an immutable variable taking the values 6 and 7.

Note that the use of the word "lifetime" above does correspond with lifetimes in Rust's ownership model, but the same would apply in any other language.

$\endgroup$
17
  • 1
    $\begingroup$ You write "during program execution", but that sounds also like a lifetime to me. What about variables that never change during program execution, but might have different values in different executions (e.g. argc and argv, or something initialised from an environment variable), would you consider those to be "constants" as well? $\endgroup$
    – Bergi
    Commented Nov 19, 2023 at 20:26
  • 5
    $\begingroup$ I'd phrase it as "its value is fixed for this whole program". Using a different value requires changing the program code, i.e. using a different program. $\endgroup$
    – Bergi
    Commented Nov 19, 2023 at 20:33
  • 2
    $\begingroup$ @Bergi Using a different value for a constant doesn't necessarily require changing the program code, for example it could depend on the size of some type on the target architecture, and hence have different values when compiled for different targets. There are different uses of terms like this, so in some contexts "constant" may mean what you describe, but my usage of the term is consistent with Wikipedia's: "a value that should not be altered by the program during normal execution". $\endgroup$
    – kaya3
    Commented Nov 19, 2023 at 20:42
  • 2
    $\begingroup$ I meant changing the program, i.e. the executed code, not necessarily changing the source code. Compiling it with different settings also creates a different program. But thanks for your clarifications, I understand what you mean now. A constant is, according to you, a global/static/top-level immutable variable. $\endgroup$
    – Bergi
    Commented Nov 19, 2023 at 20:47
  • 2
    $\begingroup$ @Longinus "In computer programming, a variable is an abstract storage location paired with an associated symbolic name, which contains some known or unknown quantity of data or object referred to as a value". No need for the value stored in that location to actually vary; blame programmers if you like, but also "In mathematics, a variable ... is a symbol that represents a mathematical object" with likewise no requirement that the object represented by the symbol must vary. $\endgroup$
    – kaya3
    Commented Nov 20, 2023 at 23:07
10
$\begingroup$

Why Have Different Syntax?

I think it's safe to say that, in general, languages make syntactic distinctions like this when

  1. a compiler could not (reasonably) determine which option to use without the distinction, but no option is an acceptable default; or when
  2. the language wants the programmer to positively opt into an option, because there are consequences.

In the case of immutable variables versus constants, I'd say it's a bit of both.

Immutable Variables and Constants

Immutable variables and constants are very similar, and some languages are happy to treat them as the same thing. I think, at least in the context of this question, we should differentiate them.

An immutable variable is simply a variable (a name referring to storage) that is immutable, by which we mean assigned at most once (or exactly once, depending on the language) at run time. This frees the compiler to make assumptions about the variable—for example, once the variable is assigned, the compiler can copy its value without worrying about synchronization.

A constant is one step further: it's a name referring to a static value, known at compile time. They don't necessarily refer to any storage—often the compiler just inlines their value into every expression that references them (depending on the language and compiler).

Different Syntax Because the Compiler Can't Decide

As I said, some languages are happy with one syntax for both immutable variables and constants. These languages may fall back to an immutable variable if the compiler can't determine that it could use a constant, or they may not have a concept of constants (as we've defined them) at all. Some languages, on the other hand, require separate syntax because it's not acceptable to default one way or the other.

One key difference between the two, in languages with pointers or references, is that immutable variables must refer to storage, and so have a defined address. On the other hand, constants need not refer to storage—depending on the language, constants may not have a defined address at all (as with C and C++ preprocessor constants), or their address may refer to a temporary copy and be different for each reference (as with Rust constants).

Also, since constants may be inlined, it may not be possible to recover their value at run time—e.g., for dynamic linking. There's not necessarily a variable to read, and if the language doesn't offer reflection the value may have been erased entirely during compilation.

Given these differences, it would never really be safe for the compiler to choose to use a constant for any public symbol. It would be disastrous if the compiler inconsistently chose to use a constant, because there would be observable differences in behavior and in ABI depending on the choice. But even if the compiler consistently chose to use a constant, it couldn't know if modules linking against this one would need to take references to the potential immutable variable, or read its value.

But it's also not acceptable for these languages to give up the ability to define constants—they're a very useful and common optimization. So, the language makes the programmer choose.

Different Syntax to Make the Programmer Positively Opt In

A language may also want a programmer to positively opt in to using a constant. Constants are generally more restrictive than immutable variables, since constants' values must be known at compile time. So the language makes the programmer intentionally choose one way or the other, even if the language could potentially make good automatic choices in some cases.

Imagine a language that has a single syntax to define immutable variables and constants, where the compiler uses a constant when it could determine the initializer's value at compile time, and creates an immutable variable otherwise. We might start out with a simple, obvious constant like

# Made up syntax...
const var FOO = 3

But this might evolve. It might get changed later to

const var FOO = 3 * 1000

Is this still a constant? It depends whether this language supports multiplication in constant expressions—many do, but if not then FOO just switched to an immutable variable. (And if multiplication is allowed, what about a function call?) If we're lucky, our change would lead to compilation errors or failed tests elsewhere in the module. But if not, there may not be any indication we changed something until we start receiving bug reports from downstream projects that link against ours.

Things would get especially tricky if the language also offered user-defined constant functions (such as C++ constexprs and Rust const fns), where a function could change from constant to non-constant and turn constants into immutable variables at a distance (or vice versa).

So instead, languages where the distinction matters tend to use separate syntax for declaring immutable variables and constants. Once the programmer has opted into one or the other, the compiler can preserve the programmer's stated intention—raising errors for misuse if necessary.

$\endgroup$
1
  • 1
    $\begingroup$ In my opion, this is the answer that is most suitable from a compiler and language design perspective and is the most suitable for this section of stackexchnage. $\endgroup$
    – m0h4mm4d
    Commented Aug 29 at 10:37
3
$\begingroup$

@apropos has covered most of this, but I just thought I'd pick up on a point about terminology and add more detail.

A "constant" is typically a field (an area of computer memory treated conceptually by the programmer as a single unit of storage) whose value is assigned in source code and stored statically in the compiled executable file, with the value specified in source code either as a literal, or perhaps as a simple expression of other constants.

Storage for a constant is allocated statically (a space is reserved in the executable itself), and the value of a constant is loaded from the executable into main memory at the same time that the whole program is loaded into main memory by the operating system. This means the value of the constant is already set before the program begins running, and the value does not change for the lifetime of the running program.

A "variable" is a field whose value is assigned (and reassigned) at run-time by the program. Unlike constants, variables can be computed at run-time from inputs that wouldn't have been available at compile-time. Unlike constants, the storage for variables can also be allocated and deallocated dynamically - that is, variables can be allocated in main memory, but have no equivalent space already reserved in the executable as constants do.

An "immutable variable" is a term for a variable that is assigned only once by the program, typically at the site of declaration. That is, fundamentally it is a variable (in the variable/constant distinction described above), except only one initial assignment is allowed. This single-assignment is enforced by the compiler.

"Immutable" may have been a misnomer originally in the sense that there is a run-time mutation, although in usage the term "immutable" has come to mean "mutable once only".

To confound things further, many languages allow variables to be initialised from literals stated in the source code, much like constants are initialised. In this case, what happens in practice is that an anonymous constant is created by the compiler to store the literal value in the executable, and then at run time the value of the anonymous constant is copied into the variable.

The essential difference between constants and immutable variables, is that constants have their values built at compile-time, whereas immutable variables have their values built at run time.

Constants are immutable across all executions of a particular compiled program, whereas immutable variables are only necessarily immutable for the lifetime of a single run of the program. You can start another run of the same program, and the value of the constant will be the same for both, but the value of the immutable variable may well differ.

Final thought: perhaps the word "irremutable" would have been a better term, capturing the idea that it is the ability to re-mutate which is excluded, not the ability to mutate at all.

$\endgroup$
12
  • 2
    $\begingroup$ From a language perspective, an immutable value is truly immutable: for the entire duration of its existence, its value does not change. Only at the implementation level can we see the memory in which this value is stored undergoing mutation during the initialization, but that's an implementation detail that languages do not concern themselves with (typically). $\endgroup$ Commented Nov 19, 2023 at 17:05
  • $\begingroup$ @MatthieuM., even a "language perspective" would have to concern itself with the question of whether a value can change across program runs or not. An "immutable" variable can change across runs, because it can be set once each time it is declared; since a program can usually accept inputs before all declarations have occured, it is possible to alter the values of immutable variables by varying the program inputs. A constant doesn't vary across runs - it can only be altered in source code. $\endgroup$
    – Steve
    Commented Nov 19, 2023 at 19:47
  • 2
    $\begingroup$ @Steve It's not "the same variable" when a program runs multiple times, any more than it's "the same" local variable each time a function is called. (Local variables being distinct is hard to argue against, since the same function can have multiple overlapping invocations concurrently, which is inconsistent with a variable holding one value at a time.) $\endgroup$ Commented Nov 20, 2023 at 4:38
  • 3
    $\begingroup$ Initialising a variable does not count as "mutation" - changing from one value to another. There is no previous value, so no change. Thus I disagree with "irremutable" $\endgroup$ Commented Nov 20, 2023 at 4:40
  • $\begingroup$ @MartinKealey, the very fact that it isn't the "same variable" on each run, is precisely what distinguishes variables from constants! As for initialising not counting as "mutation", I'm afraid that's an unsound perspective on the situation for anyone who understands computer science. The allocation of memory - defining the structure of storage which a program relies upon for its workings - is always fundamentally a different thing from assignment into that storage. The fact that some languages do both things in a one-liner, doesn't mean they become the same things. $\endgroup$
    – Steve
    Commented Nov 20, 2023 at 11:39
2
$\begingroup$

If it is a variable, regardless if immutable or not, you can get the address of it when needed (a language must support a concept of the pointer to immutable object for this to work safely). A compile time constant has no address.

An address of the immutable object can be used:

  • For easy check if it is the same object.
  • For sharing the immutable object.
  • When using immutable pointers to "user objects" like tree data nodes.
  • When passing variables of various sizes and types as uniform list of addresses.

In C++, constant references can be used to obtain the immutable address of otherwise mutable variable. This allows to track easier where the variable could probably be modified. For instance, some startup code may set the variable, and the most of the program only see a constant reference to it, making sure the variable cannot be modified any further.

$\endgroup$
1
  • 1
    $\begingroup$ Other examples of immutable objects are the ones provided at a fixed address by CPUs, microcontrollers or other hardware in the system; they can, for instance, contain a serial number or calibration values (e.g. for an internal temperature sensor or voltage reference). $\endgroup$
    – NZD
    Commented Nov 21, 2023 at 2:32
2
$\begingroup$

The different levels of const-ness are vital to lower levels of systems programming. The important distinction in systems programming between a compile time constant and an immutable variable relates to performance, the programmer's intent, and safety in the presence of external effects. Two prominent examples, Rust and C, make this distinction, and in more or less the same way.

Actual compile-time constants (that definitely are)

C has #defines that can effectively be considered direct textual substitution — copy and paste, if you like. Rust's consts are the same, except they're typed. It is useful to consider that Vec::new() is const!

Why is creating a dynamically growable array const though? Because a vec does not allocate when it's created. There are no runtime side effects at this point. The data structure that makes it up can be populated with the platform's equivalent of a null pointer and zero length entirely at compile time. This leads to confusion over this kind of code:

const CONST_VEC: Vec<String> = Vec::new();

pub fn main() {
    CONST_VEC.push("Hello".into());
    CONST_VEC.push("world!".into());
    dbg!(CONST_VEC);
}

The output of this is:

[src/main.rs:7] CONST_VEC = []

Why? Because this code effectively copy-pastes Vec::new() into each place where you see CONST_VEC. That's what a compile time constant is, a straight up substitution of compile-time-computable values. (You get warnings about this. Just to be clear. Rust knows you're doing something silly here.)

The point of this kind of copy-paste constant is simply to reduce repetition and document intent by naming, without compromising on the performance gains of static program parameters. Repetition of literals is bad because each repetition is the site for a potential mis-copy. Make the infallible machine do the copying! Naming is good, because identical literals may have different purposes, and a simple find-and-replace does not know that. Tell the fallible human which number to replace when a requirement changes!

Immutable variables (that might not be)

In a lot of cases, yes, there is no difference. Indeed in C++ a static const that is not also volatile can be treated the same as a #define or Rust const — a copy-paste. (Someone please correct me if I'm wrong, I'm not nearly as familiar with C++.)

But. In both Rust and C, their respective immutable variables can change, even within the lifetimes of the bindings. This is not some quirk or hackery, it is not unsafe, it is by design. The "immutability" is thus about what the programmer intends and declares, not the data itself.

In Rust, a non-mut variable can change due to interior mutability eg. using one of the UnsafeCell-derived types. You might regard this as a technicality or escape hatch, but interior mutability, and more importantly safe interior mutability, is a fundamental part of Rust's goal of combining usefulness and safety. UnsafeCell might expose an unsafe API, but there are plenty of safe interior mutability wrappers in Rust's stdlib eg. RefCell and Mutex.

In C, a const volatile variable is perfectly valid. What does it mean for a variable to be both const ("does not change") and volatile ("can change outside of operations known to the compiler")? One of those mental models must be wrong, and it's the first one. const simply means that the programmer, the human typing the word const, is declaring that they will not need to write code that directly changes it. The C compiler can enforce this. If the variable is not also volatile, the compiler can also make decisions about optimisations (reordering, combining, even total elision). If it is volatile, the compiler can only (but still) enforce the intent declared by the programmer.

Examples of where this commonly applies, in both languages:

  • hardware exposed as a read-only memory-mapped peripheral eg. a UART receive buffer
  • data crossing foreign function interface (FFI) boundaries
  • data shared across thread or process boundaries.

Embedded and OS programming (for example) depend upon:

  • having practical support from the language to simultaneously...
    • constrain what the programmer can mutate...
    • while accounting for the presence of externally driven side effects;
  • achieving high performance by exploiting knowledge of program parameters

Taking a higher view then, these different levels of const-ness exist because of the intersection of requirements that are especially common in (although not exclusive to) low-level applications.

$\endgroup$
2
  • $\begingroup$ What is FFI? Foreign function interface? $\endgroup$ Commented Nov 21, 2023 at 14:15
  • $\begingroup$ @PeterMortensen Yes! Sorry for the jargon, I should use the full term on first use. $\endgroup$
    – detly
    Commented Nov 21, 2023 at 15:54
2
$\begingroup$

This is an old question with many good answers but in a quick survey of them I think none mention a particularly important difference between constants and readonly variables.

In C#, named constants must semantically be constant, forever. Why is that? Because the compilation model of C# assumes that constants are constant forever. Let me illustrate how this goes wrong. Suppose you have two assemblies, Alpha.DLL and Bravo.DLL, where Bravo has a dependency on Alpha. In the source code for Alpha, you somewhere have

public const string AlphaCopyright = "Alpha Software Copyright 2024";

If Bravo somewhere in it has

Console.WriteLine("Bravo uses Alpha Corp software");
Console.WriteLine(Alpha.Whatever.AlphaCopyright);

then when Bravo is compiled, the compiler copies the constant value into Bravo.DLL. It does NOT generate code that looks up the value in Alpha.

Now suppose some user out there has a copy of Bravo.DLL and Alpha.DLL, but they update their version of Alpha.DLL to a version that has

public const string AlphaCopyright = "Alpha Software Copyright 2024-2025";

then when they run code in Bravo, it still has the original copyright notice from when it was compiled. It does not pick up the new one, even though from reading the source code, it looks like it should be picking up the latest from whatever version of Alpha is installed.

The compiler assumes that anything marked const will never change even in future versions of the source code, and optimizes accordingly, so if a value could possibly change at some point in the future then mark it as readonly, not as constant, or make it a readonly property. Alpha's developers should have declared this as a property, not a const.

$\endgroup$
3
  • $\begingroup$ I wonder how much of it is language semantics and how much of it is DLL quirks. Even with C, you could have a header with a given value for a constant, and a DLL which was compiled with a prior value, and thus no longer match. To me, this indicates not a semantic specificity of C, and more a "failure" of toolchain/distribution in not realizing the discrepancy between the two. Similarly, in your scenario, one could argue the failure is that Bravo.DLL was not properly updated when Alpha.DLL was... $\endgroup$ Commented Jun 21 at 14:09
  • $\begingroup$ ... For example, what if we shift to speaking about inline methods, rather than constants. If a method of Alpha was inlined in Bravo.DLL, and thus subsequently does not match that of Alpha.DLL, is it a developer issue because an inline method definition should never change? Or a toolchain/distribution issue because Bravo.DLL is not matching Alpha.DLL? I'd definitely argue that it's the latter. $\endgroup$ Commented Jun 21 at 14:11
  • $\begingroup$ @MatthieuM.: Though I take your point, the C# compiler never inlines code when generating IL; calls are generated as calls. As you correctly note, it would cause versioning problems if code were inlined across assemblies, but the problem is more general than that: the C# compiler has no basis upon which to decide whether inlining is an optimization or not! The jitter makes that decision because the jitter knows what the performance characteristics of the hardware are. $\endgroup$ Commented Jun 27 at 19:53

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .