Array assignment is alias and assignment semantics

Why?

I started this post because I found that array assignment created an alias, leading to an unexpected problem. I wasn’t sure if this was a bug.

But it’s now clear that it is the expected behavior. So my goals are

  1. to help others who may encounter this;
  2. to understand how assignment semantics work in general, and how to find out information on same;
  3. to take some probably ineffective shots at the documenation :grinning:.

Behavior

Apparently if aa is an Array and I set bb = aa then changing the elements of bb changes the elements of aa. See the example at the bottom if you don’t believe it! Perhaps overly influenced by R, which semantically treats the two as independent (while avoiding unnecessary copies behind the scenes), I found this surprising.

For the record, if this behavior is a problem for you, a solution is to use bb = copy(aa).

Help/Learning

How do I know what I’m getting when I do y = x? My current concern is with x having the Array type, but I’d also like to know how to find out for other types. I can think of at least 3 possibilities:

  1. y could be a completely separate copy of x;
  2. it could be some kind of view of x; or
  3. it could be an alias for x.

The question probably presupposes that = could behave differently for different types on the rhs (and what if the lhs already has a type?), and I’m not at all sure that’s true. (I am perhaps overly influenced by my experience with C++ which can do all kinds of sneaky stuff relating to assignment.)

Documentation

When I looked in the documentation I couldn’t find anything in the discussion of Arrays addressing this issue—only the semantics of assignments with indexing.

While writing this note I finally did find some stuff in the documenation.

Array assignment behavior is mentioned in the second item on Noteworthy Differences from other Languages · The Julia Language, which is for MATLAB (which I ignored since I don’t use it). It doesn’t seem to be in the discussion of differences from R.

Essentials · The Julia Language has an entry for the “keyword” =. It says

For variable a and expression b, a = b makes a refer to the value of b .

which IMO doesn’t help that much. But the examples section does go on to make it crystal clear for arrays:

Assigning a to b does not create a copy of b ; instead use copy or deepcopy .

which seems like a point that should have appeared earlier. This is followed by an example in the same spirit as my own session, appearing next.

Example


julia> aa = [1, 2, 4]
3-element Vector{Int64}:
 1
 2
 4

julia> bb = aa
3-element Vector{Int64}:
 1
 2
 4

julia> bb[1:2] = zeros(Int64, 2)
2-element Vector{Int64}:
 0
 0

julia> aa
3-element Vector{Int64}:
 0
 0
 4

julia> bb[3] = 88  # Maybe simple indexed assignment is different?
88

julia> aa
3-element Vector{Int64}:
  0
  0
 88
3 Likes

The reason is that there is nothing special about Arrays : all assignments, to any kind of type, could be seen as an alias. As defined in the manual section on variables (the first one after “Getting started”):

Which is actually “an alias”. The problem you are describing is related to variables that refer to mutable values (of which arrays are only a single case). If you have multiple variables bound to the same value and it is mutable, a change in any of those variables is reflected in the all the others.

2 Likes

Values vs. Bindings: The Map is Not the Territory · John Myles White is a good post about this.

In Julia, it might be useful to think about “objects” (integer, arrays, strings etc) living in one place by themselves. And then you have variables that map to these objects. An assignment is simply creating a mapping between the variable and the object the right-hand side evaluates to. By looking at it like that, it is clear that an assignment can never mutate anything. And it is also easy to see that a = b just creates a mapping from the variable a to the object that b evaluates to (the object to which it maps to). In the end, you just have two names referring to the same object.

Objects themselves can be mutated. This is done via e.g. a[1] = 1.0 (setindex!) and s.a = 3 (setproperty!). In the view above, mutating an object clearly means that retrieving that object in any way (using any of the names/variables for it) will see that mutation.

5 Likes

The documentation can always be improved… In particular for what’s missing in the list of differences with R, at the top of the page, right corner, you can click “Edit on GitHub” (then click on the pencil icon) to propose a change.

1 Like

This is mostly true, with the exception of closures. Assignment to a closed-over variable does mutate an object (the hidden Core.Box that is added during lowering).

You can mention other things like assigning to a non-const global which will in the end call the runtime and “store” (julia/module.c at 66c9f6a9b58130d35fa9cf10a7e16daf5847e575 · JuliaLang/julia · GitHub) something (so it is not a no-op). However, when talking about things like this it is important not to start talking about the implementation of the language. The fact that you mentioned “lowering” and “hidden” makes it feel like we are now, in fact, talking about implementation.

1 Like

Well, I think that “assignment in julia is non-mutating” is a super nice slogan.

But it needs both qualifiers in order be obviously and unambiguously correct: “Assignment in julia is non-mutating (except for globals and except for closed-over variables; then it’s subtle and depends on the precise definition of the word mutation)”.

One can reasonably argue whether assignment to globals or assignment to closed-over variables should be considered mutation. The very fact that this is a subtle argument about the semantics of the word “mutation” and implementation details is imo enough to warrant that qualification.

This is an extract of previous answers that are probably helpful:

https://m3g.github.io/JuliaNotes.jl/stable/assignment/

In simple terms (which are the ones I can understand): You are always just giving a new label to the same value stored in the position in memory for which x is also a label:*

x = 3
y = x # points to the same value as x, thus y = 3
x = 4 # this simply assigns the x label to a new value, thus not affecting y

That is exactly the same for arrays:

x = [1,2]
y = x
x = [3,4] # x label bound to a new value, y unaffected

The confusion arises with the mutation syntax:

x = [1,2]
y = x
x[1] = 3
y == [3,2] # y was mutated

that is because one must distinguish x[i]= from x= : the x[i] notation is a syntactic sugar to the setindex! function, which does something different (mutates in place) the value in a specific position in memory.

*footnote: It is normal to initially distinguish arrays from scalars in what concerns that behaviour, because for scalars you cannot really distinguish if the new label points to the same position in memory or not than the previous scalar, since the values are the same anyway. Actually, it will be the compiler which will decide if one thing or the other will happen, depending on the context of the code. For arrays the copy and assignment are effectively distinguishable from the user point of view, and two different notations exist for them.

2 Likes