Simple question about assignment to a vector of vectors

Example:

julia> A = fill(fill(0,2), 3)
3-element Vector{Vector{Int64}}:
 [0, 0]
 [0, 0]
 [0, 0]

julia> A[1][2] = 5
5

julia> A
3-element Vector{Vector{Int64}}:
 [0, 5]
 [0, 5]
 [0, 5]

The result is not what I would expect.
Instead, I would have expected to get

julia> A
3-element Vector{Vector{Int64}}:
 [0, 5]
 [0, 0]
 [0, 0]

Can you explain to me what’s going on here, and why the result is not what I expect?

Another data point. When the vectors are different sizes, then I get the behavior I expect:

julia> A = [[0,1], [2,3,4], [5,6]]
3-element Array{Array{Int64,1},1}:
 [0, 1]
 [2, 3, 4]
 [5, 6]

julia> A[1][2] = 10
10

julia> A
3-element Array{Array{Int64,1},1}:
 [0, 10]
 [2, 3, 4]
 [5, 6]

Ah yeah this tripped me up at first as well. If you look at the help page for fill:

help?> fill

If x is an object reference, all elements will refer to the same object:

  julia> A = fill(zeros(2), 2);
  
  julia> A[1][1] = 42; # modifies both A[1][1] and A[2][1]
  
  julia> A
  2-element Vector{Vector{Float64}}:
   [42.0, 0.0]
   [42.0, 0.0]

All of the objects in A are the same (like pointing exactly to the same spot in memory). So when you update one, they all get updated.

I usually avoid this using a list comprehension:

julia> B = [zeros(2) for _ in 1:3]
3-element Vector{Vector{Float64}}:
 [0.0, 0.0]
 [0.0, 0.0]
 [0.0, 0.0]

julia> B[1][2] = 5
5

julia> B
3-element Vector{Vector{Float64}}:
 [0.0, 5.0]
 [0.0, 0.0]
 [0.0, 0.0]
4 Likes

Thanks for the help!

I guess I was confused because it didn’t occur to me that the internal fill(0,2) would be creating a new object that persists. I would have realized what’s happening if I had written this instead:

julia> b = fill(0,2)
2-element Array{Int64,1}:
 0
 0

julia> A = fill(b, 3)
3-element Array{Array{Int64,1},1}:
 [0, 0]
 [0, 0]
 [0, 0]

julia> A[1][2] = 5
5

julia> A
3-element Array{Array{Int64,1},1}:
 [0, 5]
 [0, 5]
 [0, 5]

1 Like

I think this is a really important thing for people to come to Julia (especially from R) to realize. In Julia, whenever you call f(g(x)) f only sees the result of g(x), not how it got there. This is why macros have special syntax. In R, all functions are the equivalent of macros in Julia, which adds a ton of magic that makes code less predictable. In Julia, there is a special syntax that warns the reader that something special is happening when a macro is used.

4 Likes

@Oscar_Smith, could you please explain a bit further what this fill()'s behavior has to do with f(g(x)), and why this is the most logical choice?
It looks like another trap that is very easy to fall into. Thanks.

A = fill([0,0], 3)
A[1][2] = 5
julia> A
3-element Vector{Vector{Int64}}:
 [0, 5]
 [0, 5]
 [0, 5]
3 Likes

First, if all you want is a matrix, then fill(0, (2, 3)) suits your need.
If you really want an array of arrays, then you have to create independent arrays for each entry in the larger array (usually with list comprehension).

For your question, fill([1], 3) could be interpreted as such:

  • create an array that repeats a single object
  • the object is [1] in this case
  • this object will be repeated for 3 times
  • fill does not attempt to make copies of this object

But array comprehension [[1] for i in 1:3] is not taking [1] as an argument, instead, it takes a function i->[1] that produces a distinct array that only contains 1 each time it is called.

3 Likes

see also fill(anArray,2) behaviour

2 Likes

I agree that the behavior of fill([0, 0], 2) is counterintuitive and kind of pointless. This question comes up on a regular basis here on Discourse. I would favor changing this behavior for Julia 2.0.

1 Like

I just opened an issue that suggests changing the fill behavior for Julia version 2.0.

https://github.com/JuliaLang/julia/issues/41209

2 Likes

What is going on here?

julia> a=fill(zeros(2),2)
2-element Vector{Vector{Float64}}:
 [0.0, 0.0]
 [0.0, 0.0]


julia> a[1]=[1,1]
2-element Vector{Int64}:
 1
 1

julia> a
2-element Vector{Vector{Float64}}:
 [1.0, 1.0]
 [0.0, 0.0]

What is going on here? Why didn’t I get:

 [1.0, 1.0]
 [1.0, 1.0]

?

a = fill(zeros(2), 2)

this creates a 2-element vector, where both elements refer to the same object, namely the newly created zeros(2).
i.e. a[1] refers to the same object as a[2]. This is the behaviour of fill.

If you modify (mutate) that object, the change will be reflected when referencing that object via either a[1] or a[2] (since it is the same object).

a = fill(zeros(2), 2)
a[1][1] = 42.0    # note that a[2][1] = 42.0 will do the same thing
a

produces

2-element Vector{Vector{Float64}}:
 [42.0, 0.0]
 [42.0, 0.0]

However, when you write a[1] = [1.0,1.0], you are “reassigning” a[1] to refer to the newly created object [1.0,1.0]. Hence, a[1] and a[2] now refer to different objects.

4 Likes

I also stumbled in the same way. This can cause someone to waste many hours looking for errors in their code, while the mistake was to rely too much on their own intuition.