Weird behavior of for loop+copy syntax

Dear All
Here is a minimum working example:

function test_func()
   A=[zeros(Float64,2) for _ in 1:2]
   B=copy(A)
   for ja in 1:2, jb in 1:2
       (B[ja][jb],A[ja][jb])=(1.0,2.0)
       
   end
   print(B)
   print(A)
end

When I call this function, it prints out two matrix, with 2.0 on all its entries

Why does Julia behave in this way? When I write B=copy(A), shouldn’t they refer to different object rather than the same reference? Then when I try to assign all the entries of B to be 1, and all the entries of A to be 2, why doesn’t this grammar work?

Welcome to the trickiness of copying. In Julia (and many other languages), copy is a shallow copy that only duplicates the outside structure.

Here A is a Vector{Vector{Float64}} and so only the outermost Vector is copied, the internal Vector{Float64}s are not. So both A and B end up with the same contents and you get the behaviour you are experiencing.

What is confusing is that types like Float64 aren’t references so if you copy a Vector{Float64} you do end up with distinct vectors. This can be shown with a simple example:

A = [1, 2]
B = copy(A)
A[1] = 2
B[1] == 1 # true
2 Likes

works with

 @doc deepcopy  
1 Like

It’s easier and consistent to think of a shallow copy of a mutable data structure as a copy sharing elements/instances with the original.

julia> x = [1];

julia> x2 = copy(x);

julia> x[begin] === x2[begin]
true

julia> y = [[1]];

julia> y2 = copy(y);

julia> y[begin] === y2[begin]
true

The elements of y and y2 both contain the same instance [1], so mutating that instance by reassigning its element will be reflected in both y and y2. This doesn’t apply to x’s immutable elements.

julia> y[begin][begin] = 2; # mutates y[begin], not y

julia> y, y2
([[2]], [[2]])

You can mutate x by reassigning its element, and that will not occur to the copy x2. The same thing applies to y:

julia> x[begin] = 2;

julia> x, x2
([2], [1])

julia> y[begin] = [3];

julia> y, y2
([[3]], [[2]])

The confusion comes from 1) failing to tell what is being mutated, and 2) failing to distinguish a matrix of scalars from a vector of vectors. A matrix works fine:

julia> function test_func2()
          A=zeros(Float64, 2, 2) # makes a 2x2 matrix
          B=copy(A)
          for ja in 1:2, jb in 1:2
              (B[ja, jb],A[ja, jb])=(1.0,2.0) # mutates B and A, not its elements
          end
          print(B)
          print(A)
       end
test_func2 (generic function with 1 method)

julia> test_func2()
[1.0 1.0; 1.0 1.0][2.0 2.0; 2.0 2.0]

but so would a vector of vectors if you mutated the outer vector instead of the inner ones, though this is much worse than a proper matrix for other reasons:

julia> function test_func3()
          A=[zeros(Float64,2) for _ in 1:2]
          B=copy(A)
          for ja in 1:2
              (B[ja],A[ja])=([1.0, 1.0],[2.0, 2.0]) # mutates B and A, not its elements
          end
          print(B)
          print(A)
       end
test_func3 (generic function with 1 method)

julia> test_func3()
[[1.0, 1.0], [1.0, 1.0]][[2.0, 2.0], [2.0, 2.0]]

I assume it’s written this way for the purpose of a MWE, there are much better ways to make arrays filled with the same number.

2 Likes