Hi, I use Julia1.10.4 in Archlinux.
I try to use zip to dynamically create a named tuple, like this
test = (; zip((:a, :b), fill(zeros(3), 2))...)
=> (a=[0.0, 0.0, 0.0], b=[0.0, 0.0, 0.0])
However, very strange, when I make a change to the a
filed, the b
is also altered unexpectedly!
test.a[1] = 1.0
=> (a=[1.0, 0.0, 0.0], b=[1.0, 0.0, 0.0])
Is this a feature of zip or simply a bug? Found this really confusing.
If I do the named tuple construction manually instead of using zip, I get (a=[1.0, 0.0, 0.0], b=[0.0, 0.0, 0.0])
, which is what I am expecting.
The issue is coming from fill
which doesn’t instantiate copies, but fills the container with references to the same input object.
julia> x = fill(zeros(3),2)
2-element Vector{Vector{Float64}}:
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
julia> x[1] === x[2]
true
julia> x[1][1] = 1.0
1.0
julia> x
2-element Vector{Vector{Float64}}:
[1.0, 0.0, 0.0]
[1.0, 0.0, 0.0]
You can get the expected behavior with map
julia> test = (; zip((:a, :b), map(_ -> zeros(3), 1:2))...)
(a = [0.0, 0.0, 0.0], b = [0.0, 0.0, 0.0])
julia> test.a[1] = 1.0
1.0
julia> test
(a = [1.0, 0.0, 0.0], b = [0.0, 0.0, 0.0])
1 Like
Hi @mrufsvold, thanks for such quick and detailed response!
I see the point very clearly. Coming from matlab, I find this copy type of programming error really hard to debug. Took me a whole night locating this in my code.
1 Like
Very smart folks thought through these APIs, and I don’t mean to backseat drive them, but I resonate with friction in places like this!
I have a difficult time thinking of a use case where I actually want a vector of copies of the same thing, seems like fill
should default to using copy
unless told otherwise.
1 Like
Fill works well with immutable elements, i.e., value types, such as numbers: fill(1.23, 4)
. For mutable stuff, comprehensions are an easy and readable alternative: [zeros(3) for _ in 1:4]
(albeit slightly longer).
1 Like
Totally correct and exactly what the docstring recommends, but I still don’t agree that this is the right design. The fact that the docs need to have a caveat because this problem comes up so much is evidence to me that handling mutable objects differently would be better.
Fair enough, maybe fill
could be extended with methods taking factory functions, e.g., fill(() -> zeros(3), 2)
, or a wrapper type marking copies, e.g., fill(Copied(zeros(3)), 2)
?
1 Like
I love the idea of a Copied
type! But certainly an anonymous function would be lower friction.
Although, I guess it is technically breaking since you could right now call fill(f, 3)
to get an array of functions.