When I create some array A and assign it to B
A = [1:10]
B = A
I can modify A and the change reflects in B
A[1] = 42
# B[1] is now 42
But if I do that with scalar variables, the change doesn't propagate:
a = 1
b = a
a = 2
# b remains being 1
I can even mix the things up and transform the vector to a scalar, and the change doesn't propagate:
A = [1:10]
B = A
A = 0
# B remains being 1,2,...,10
What exactly does the =
operator does? When I want to copy variables and modify the old ones preserving the integrity of the new variables, when should I use b = copy(a)
over just b=a
?
This behavior is similar to Java. A and B are variables that can hold either a "plain" data type, such as an integer, float etc, or a references (aka pointers) to a more complex data structure. In contrast to Java, Julia handles many non-abstract types as "plain" data.
You can test with
isbits(A)
whether your variable A holds a bit value, or contains a reference to another data object. In the first caseB=A
will copy every bit fromA
to a new memory allocation forB
, otherwise, only the reference to the object will be copied.Also play around with
pointer_from_objref(A)
.The confusion stems from this: assignment and mutation are not the same thing.
Assignment. Assignment looks like
x = ...
– what's left of the=
is an identifier, i.e. a variable name. Assignment changes which object the variablex
refers to (this is called a variable binding). It does not mutate any objects at all.Mutation. There are two typical ways to mutate something in Julia:
x.f = ...
– what's left of the=
is a field access expression;x[i] = ...
– what's left of the=
is an indexing expression. Currently, field mutation is fundamental – that syntax can only mean that you are mutating a structure by changing its field. This may change. Array mutation syntax is not fundamental –x[i] = y
meanssetindex!(x, y, i)
and you can either add methods to setindex! or locally change which generic functionsetindex!
. Actual array assignment is a builtin – a function implemented in C (and for which we know how to generate corresponding LLVM code).Mutation changes the values of objects; it doesn't change any variable bindings. After doing either of the above, the variable
x
still refers to the same object it did before; that object may have different contents, however. In particular, if that object is accessible from some other scope – say the function that called one doing the mutation – then the changed value will be visible there. But no bindings have changed – all bindings in all scopes still refer to the same objects.You'll note that in this explanation I never once talked about mutability or immutability. That's because it has nothing to do with any of this – mutable and immutable objects have exactly the same semantics when it comes to assignment, argument passing, etc. The only difference is that if you try to do
x.f = ...
when x is immutable, you will get an error.