Maybe it’s just the classic confusion between reassignment of a variable and mutation of an instance; this is common for people coming from languages where “a variable” is basically a named instance. Although the separation of variable and instance along with mutability/immutability is a safer and simpler abstraction than pointer logic, it’s not entirely straightforward, to be fair.
It helped me to first imagine an immutable instance to have a primary sequence of bytes, and that’s exactly how they are identified; if two immutable instances have the same sequence, they are the same, even if you constructed them separately. A mutable instance however is an address to a remote data, and it’s identified by that address; separate copies of equal data at separate addresses are different instances. This works out for immutable structs with mutable fields, because its primary sequence only contains the address of the mutable field instance, not its remote data. When you mutate an instance, you change the remote data, not its address, so the identity remains the same.
A variable is just a named reference to an instance so that your code has access; 1 instance can have many references. Assignments are how you make a reference to an instance. You use a variable to access an instance in order to mutate it, e.g. am.num = 1
, arr[2] = 5
, push!(arr, 3)
, but that is not the same as reassigning that variable to a possibly different instance e.g. am = MutInt(1)
. It might look similar sometimes, and indeed in am.num = 1
you are reassigning the field .num
of the instance to mutate it, but the variable am
itself is just used for access, not being reassigned.
By this point, it should be clear that B = A
in the OP accesses the variable A
to get an instance, then assigns the same instance to variable B
. That instance has 2 variables A
and B
, and there was only 1 instance to mutate, no matter which variable is used to access it. To assign a separate instance to B
, you need to create a separate instance, e.g. B = copy(A)
or B = A + 0
.
Open to see behavior of variables with immutable instances. Note that no instance is ever mutated.
julia> a = 3
3
julia> b = a # access variable a, get instance 3, assign 3 to variable b
3
julia> a === b # same instance has 2 variables
true
julia> a += 1 # compute a+1, then reassign result to a
4
julia> a === b # different instance because immutable value differs
false
julia> b = 4 # make instance 4, assign 4 to variable b
4
julia> a === b # same instance because immutable value is same
true
Open to see behavior of variables with mutable instances. Note that the reassignment behavior is the same as immutable ones, the difference is mutability and identity rules.
julia> mutable struct MutInt num::Int end
julia> Base.:+(x::MutInt, y::Int) = MutInt(x.num+y)
julia> am = MutInt(3)
MutInt(3)
julia> bm = am # access variable am, get instance, assign it to variable bm
MutInt(3)
julia> am === bm # same instance has 2 variables
true
julia> am.num = 1 # access variable am, mutate its instance by reassigning its field num
1
julia> am === bm # 2 variables still access the same instance
true
julia> am += 1 # compute am+1, then reassign result to am
MutInt(2)
julia> am === bm # different instance because mutable addresses differ
false
julia> bm.num = am.num # mutate so fields are equal
2
julia> am === bm # different instance because mutable addresses differ
false