Modifying the fields of an immutable struct

I have defined a struct

struct foo
x 
y 
end

Then I created a variable

a = foo([1,2], [3,45])

I got error with

 a.x= [3,4]
ERROR: setfield!: immutable struct of type foo cannot be changed
Stacktrace:
 [1] setproperty!(x::foo, f::Symbol, v::Vector{Int64})
   @ Base .\Base.jl:38
 [2] top-level scope
   @ REPL[85]:1

But it is okay for

julia> a.x[1]= 3
3

julia> a
foo([3, 2], [3, 45])

I do not understand the mechanism behind.

Why is it not possible to modify ‘a.x’ as a whole but possible to modify it partially?

For a detailed discussion see Types · The Julia Language.

In short: The objects of a struct can not be changed. For your example

If you would do a=foo(1,2) then a.x is 1 and that can not be changed. You can not do a.x=2
So the Array [1,2] you store in x can also not be changed or let’s say _ex_changed with another array, like you try in your assignment. Maybe best have in mind that the memory, type and so on are fixed.

But for any array you can change the values of course. That is still the same memory!
And you can circumvent this a bit by writing the assignment with broadcast a.x .= [2,3] since this would not exchange the whole array but the single entries.

4 Likes

a is immutable but the Vector stored in a.x is mutable (like any Vector).

4 Likes

Maybe one could also say that the elements of an immutable struct need to be === to themselves over the whole lifetime of the struct, but for a Vector that doesn’t imply what the stored elements are, just its identity

4 Likes

Sorry for the long post, but this gets into the “why.” Despite the name, mutability is not just about change, it’s about variables sharing an instance’s data.

julia> x = y = [1]; # 2 variables to same instance

julia> x[1] = 2; (x, y)
([2], [2])

As you can see, the 2 variables share the same instance, including its mutations. There is a way to check for this identity:

julia> x === y # checks if 2 inputs share 1 instance
true

If we instantiate a mutable type twice, you get 2 instances, even if starting at equal value:

julia> x = [1]; y = [1]; x === y
false

julia> x == y # implemented to check for equal value
true

You can think about it as two independent memory addresses (it’s how it’s implemented). They can mutate separately, and if the segments happen to mutate to the same value again, they won’t merge into one. An instance’s address is fixed so that many variables can have their own copies of the address to find the instance; if it could change, you would need to track and change multiple copies, which isn’t feasible. So, the unique address ironically is not stored at a unique address.

Immutable values have a different kind of identity. Equality (implemented as comparing bits content) means the same instance, even if they were instantiated separately:

julia> x = 1; y = 1; x === y
true

Immutable values can still be involved in change, but only by reassigning a different instance to a variable, one at a time. You can’t change the instance itself in any way, you can’t reassign any field or element of it.

julia> x = y = 1;

julia> x = 2; (x, y)
(2, 1)

At this point, you might be thinking that mutables are more flexible and more straightforwardly have each instantiation make a new instance with its own address, so why do immutables exist? We have to talk briefly about stack memory and heap memory. It’s a fairly complicated subject, but you don’t need to know much here.

A function call puts a stack frame holding data onto the stack; when the call is done, the frame vanishes. If a mutable instance is stored in the stack frame, the instance vanishes when the call is over; you’ll never be able to return it. So, mutables are usually stored in heap memory. Unfortunately, allocating data on the heap is slower, and the garbage collector has to stop the program to figure out if a mutable instance is no longer needed and delete it. It’s common practice to reduce heap allocations as much as possible in performance-critical code, and this often means reducing instantiation of mutable types. A compiler optimization is proving a mutable instance does not escape the function, so it can be allocated on and freed by the stack instead.

An immutable instance cannot mutate and is not identified by address, so it can be copied around in memory, just like the address of a mutable instance. If an immutable instance is stored in the stack frame, the value can just be copied outside the stack frame to return before the stack frame vanishes. If a variable is reassigned a different immutable instance with a fixed size, you don’t need a different address, you can just overwrite the same segment of stack memory. Another way that not needing a separate address helps is inlining immutable elements into an array or struct so the garbage collector only needs to track that 1 array or struct, not all the different elements and fields.

In the same vein, a mutable’s fixed address gets to be on the stack or inlined. There are data structures with memory of unfixed size that move around on the heap for more room, but the primary address will remain the same, it’ll just hold another varying address. As you’ve noticed, an immutable struct stores fixed addresses of the contained mutable instances, so you can’t reassign different instances to the fields. However, you can use the fields to access and mutate the mutable instances.

9 Likes

My understand is rather simple.
When an object is mutable, in the struct is stored just its memory address, not the whole object (and this goes recursivelly, including in an Array, that is just another struct).
It is this memory address that, in a immutable struct, can’t change, but whtever is stored in that memory address can fully change.