Note sure if this qualifies for you, but in functional languages like Standard ML everything is immutable by default. Mutation is supported through a generic ref
erence type. So an int
variable is immutable, and a ref int
variable is a mutable container for int
s. Basically, variables are real variables in the mathematical sense (an unknown but fixed value) and ref
s are "variables" in the imperative programming sense - a memory cell that can be written to and read from. (I like to call them assignables.)
I think the problem with const
is two-fold. First, C++ lacks garbage collection, which is necessary to have non-trivial persistent data structures. const
must be deep to make any sense, yet having fully immutable values in C++ is impractical.
Second, in C++ you need to opt into const
rather than opt out of it. But when you forget to const
something and later fix it, you'll end up in the "const poisoning" situation mentioned in @RobY's answer where the const
change will cascade throughout the code. If const
was the default, you wouldn't find yourself applying const
retroactively. Additionally, having to add const
everywhere adds a lot of noise to the code.
I suspect the mainstream languages that followed (e.g. Java) were heavily shaped by C and C++'s success and way of thinking. Case in point, even with garbage collection most languages' collection APIs assume mutable data structures. The fact that everything is mutable and immutability is seen as a corner case speaks volumes about the imperative mindset behind popular languages.
EDIT: After reflecting on greenoldman's comment I realized that const
isn't directly about the immutability of data; const
encodes into the type of the method whether it has side effects on the instance.
It's possible to use mutation to achieve referentially transparent behavior. Suppose you have a function that when called successively returns different values - for example, a function that reads a single character from stdin
. We could use cache/memoize the results of this function to produce a referentially transparent stream of values. The stream would be a linked list whose nodes will call the function the first time you try to retrieve their value, but then cache the result. So if stdin
constains Hello, world!
, the first time you try to retrieve the value of the first node, it'll read one char
and return H
. Afterwards it'll continue to return H
without further calls to read a char
. Likewise, the second node would read a char
from stdin
the first time you try to retrieve its value, this time returning e
and caching that result.
The interesting thing here is that you've turned a process that's inherently stateful into an object that's seemingly stateless. However, it was necessary to mutate the object's internal state (by caching the results) to achieve this - the mutation was a benign effect. It's impossible to make our CharStream
const
even though the stream behaves like an immutable value. Now imagine there's a Stream
interface with const
methods, and all your functions expect const Streams
. Your CharStream
can't implement the interface!
(EDIT 2: Apparently there's a C++ keyword called mutable
that would allow us to cheat and make CharStream
const
. However, this loophole destroys const
's guarantees - now you really can't be sure something won't mutate through its const
methods. I suppose it's not that bad since you must explicitly request the loophole, but you're still completely reliant on the honor system.)
Secondly, suppose you have high-order functions - that is, you can pass functions as arguments to other functions. const
ness is part of a function's signature, so you wouldn't be able to pass non-const
functions as arguments to functions that expect const
functions. Blindly enforcing const
here would lead to a loss of generality.
Finally, manipulating a const
object doesn't guarantee that it's not mutating some external (static or global) state behind your back, so const
's guarantees aren't as strong as they initially appear.
It's not clear to me that encoding the presence or absence of side effects into the type system is universally a good thing.