Why `const` fields in a `mutable struct` instead of `mutable` field in ordinary `struct`?

The documentation praises immutable structs:

Composite objects declared with struct are immutable; they cannot be modified after construction. This may seem odd at first, but it has several advantages:

  • It can be more efficient. Some structs can be packed efficiently into arrays, and in some cases the compiler is able to avoid allocating immutable objects entirely.
  • It is not possible to violate the invariants provided by the type’s constructors.
  • Code using immutable objects can be easier to reason about.

So, struct Thing field end creates a type whose instances will be immutable. You have to explicitly opt into mutability using mutable struct Thing field end.

The next section of the documentation says:

In cases where one or more fields of an otherwise mutable struct is known to be immutable, one can declare these fields as such using const

This lets me define a mutable struct with some const fields:

mutable struct I_am_mut
   mutate_me::Int
   const immutable::Int
end

Here, I have to explicitly opt into immutability.


Why this dissonance:

  • Structs are fully immutable. I have to opt into full mutability by using mutable struct. There’s no partial mutability for structs.
  • mutable structs are fully mutable. I can opt into partial immutability by using const.

Why not make struct “partially mutable”, similar to how mutable struct can be partially immutable? Like this:

struct PartiallyMut
   immutable::Int
   mutable mutate_me::Int
end

With this design, mutability is always opt-in, with a gradual scale:

  1. struct is immutable by default: full immutability.
  2. Parts of a struct can be explicitly marked mutable: partial mutability.
  3. The entire struct can be marked mutable with mutable struct: full mutability.

Today’s mutable struct with const fields could’ve been a basic struct with mutable fields.

This also makes mutable struct a generalization of making individual fields mutable.


Does using mutable structs with const fields provide additional benefits? Why was such a choice made?

1 Like

If you have an immutable struct it’s nice to be able to count on it being immutable.

There was some discussion on the PR which implemented the new functionality after the same thing was brought up.

6 Likes

From this comment:

In contrast, if you start with a mutable struct nothing special is needed to make one field const; you can just disallow mutating it.

Indeed, const fields in a mutable struct seem to be much simpler to implement.

I also agree with “a mutable struct with some fields marked const is still mutable, while the opposite is not true (immutable struct with mutable field is no longer immutable)”.


The thing is, I was just trying to make a struct with exactly one mutable field and found my code littered with const fields which looked ugly to me, so I wondered why it was designed this way and of course didn’t find the PR discussion…

1 Like

An easy way if you just need one field is to use inner mutability:

julia> struct MyStruct
          immutable::Int
          reference::Ref{Int}
       end

julia> st = MyStruct(7, Ref(8))
MyStruct(7, Base.RefValue{Int64}(8))

julia> st.reference[] = 9
9

julia> st
MyStruct(7, Base.RefValue{Int64}(9))

5 Likes

IMO the best short answer is that a struct with some fields mutable and some fields const is a mutable struct. If we had had this feature before 1.0, it seems entirely possible to me that we wouldn’t have mutable struct at all and you would have to opt-in to mutability for each mutable field.

1 Like

Note that is likely worse for performance, as the days won’t be stored inline

1 Like

I’m pretty sure this is the main reason but I’m not sure if it’s documented anywhere. It seems like a good performance tip or maybe just part of the const docstring

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 their 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.
25 Likes

Having const fields in mutable structs is fine, as the const keyword placed here is just to tell the compiler/runtime to conform some user-specified restrictions. It is then just syntax sugars.

Having mutable fields in structs can be quite different.

Considering the memory model here, the first to notice is that the mutable fields in structs must have the same memory model as Ref/RefValue fields. Otherwise, mutable fields do not equal to a RefValue, and have to be implemented by allowing modifications to a value allocated on stack, which then introduces an user-observable discrepancy when passing arguments by reference/value.

So, a mutable “field” in immutable structs has to be a RefValue with generated getters/setters. Consequently, getfield for such structs is not getfield any more, but actually getproperty. Should fieldtypes return the underlying types for mutable fields? What does fieldoffset mean for mutable fields? Some reflection-based Base functions can get broken and need to be redesigned for the new semantics, but after all the need for mutable fields can be a macro thing? Does it worth? Might not…

1 Like