There’s one important distinction between a mutable struct with all const fields and an immutable struct. Consider these:
mutable struct MutableConst
const f::Int
end
struct Immutable
f::Int
end
The same, right? Not quite:
julia> MutableConst(1) === MutableConst(1)
false
julia> Immutable(1) === Immutable(1)
true
Why? Because the defining property of mutable structures is that they have object identity. What is object identity? It’s whether or not two values of a type with the same content are considered to be the same object or not. If a type has object identity, then they are distinct; if a type lacks object identity, then they are the same. A mutable type must have object identity since otherwise if you modify one copy of the object the other copy will remain unchanged, which immediately reveals that they aren’t actually the same object. Object identity in turn implies that objects are associated with a specific location in memory, since that’s how you ensure that all the references to that same object see the same content. The fact that immutable structs aren’t tied to a specific location in memory is a huge part of why they are so much more optimizable than mutable structs: you’re free to make a copy of an immutable value to pass as an argument or keep in a register—and crucially, it’s semantically the same object. If you try to do that with a mutable object you need to make sure your copy stays in sync with “the original”, which can be challenging, if not impossible.
Back to the example. You can only observe the difference between two instances of MutableConst
by mutation—which you’re not allowed to do—but we still respect their distinct object identity. The defining property of immutable structs, on the other hand is that they lack object identity, so if two immutable objects have the same content, then they are the same value—you’re not allowed to look at their memory address to distinguish them. Whereas with mutable objects, their memory address is the only thing you need to look at to tell if they’re the same object. Note that ===
is not overloadable, so this is a fundamental and unchangeable feature of the language, not how some API happens to be implemented.
Even though const
fields in mutable structs are a new feature, the same difference can actually be demonstrated in earlier Julia versions with empty structs:
# Julia 1.0:
julia> mutable struct EmptyMutable end
julia> struct EmptyImmutable end
julia> EmptyMutable() === EmptyMutable()
false
julia> EmptyImmutable() === EmptyImmutable()
true
Both structs have no fields which can be modified, so EmptyMutable
, like MutableConst
, is a mutable struct with no mutable fields. Even though the object cannot be changed, we respect its object identity and distinguish multiple instances of it. Object identity also sheds some light on why we can have const fields in mutable structs but not mutable fields in immutable structs:
- You can respect object identity while disallowing modification of fields: just allocate a new object each time you construct an instance and consider them the same object if and only if they have the same memory location; you simply disallow changing some of the fields.
- You cannot allow modification of fields without object identity: if you have multiple copies of the same value, they must behave indistinguishably, which is immediately violated if you allow mutation, since you can change one copy while the other copy will remain unchanged.