Unexpected change of a function argument (array) - bug or feature?

When doing some first coding with Julia, I run into an issue that a function argument was changed unexpectedly. I could boil the problem down to the following lines of code:

function myfunc(f,n)
q = f
for k in 1:n
q[k] += + 1.1
end
return q
end

n = 1
f = ones(1)
println(“f =”,f)
my_q = myfunc(f,n)
println(“After calling of myfunc(f,n) f is modified:”)
println(“f =”,f)
=========== output ========
f =[1.0]
After calling of myfunc(f,n) f is modified:
f =[2.1]

My question is why f in myfunc(f,n) is changed from 1.0 to 2.1? This is completely unexpected for me, because the function myfunc() should calculate q but should not change the argument f.

I’m not sure if this is a bug or a feature of Julia. I’ve used Julia version 1.7.1.

Any comments?

It’s a feature.

When you type q = f you are making q point to the same array as f. So whatever changes you make to q are also visible in f. This is extremely useful, but you must be aware of it.

Whenever f is a mutable data structure, q = f followed by mutation of q will be visible in f.

If you want to avoid this, write q = copy(f) instead. (In some cases you even need q = deepcopy(f).)

BTW: read here to see how to format your code nicely: Please read: make it easier to help you

3 Likes

What does copy do compared to deepcopy?

Kind regards

It’s helpful to read the docs in this scenario:

help?> deepcopy
search: deepcopy

  deepcopy(x)


  Create a deep copy of x: everything is copied recursively, resulting in
  a fully independent object. For example, deep-copying an array produces
  a new array whose elements are deep copies of the original elements.
  Calling deepcopy on an object should generally have the same effect as
  serializing and then deserializing it.

  While it isn't normally necessary, user-defined types can override the
  default deepcopy behavior by defining a specialized version of the
  function deepcopy_internal(x::T, dict::IdDict) (which shouldn't
  otherwise be used), where T is the type to be specialized for, and dict
  keeps track of objects copied so far within the recursion. Within the
  definition, deepcopy_internal should be used in place of deepcopy, and
  the dict variable should be updated as appropriate before returning.

Thank’s a lot for your swift answer!

Maybe a follow up question is then; for which cases would one want to use “copy” only?

Seems to me that copying the outer shell but keeping the inner references, would not have any practical use case? (I.e. I am asking; when would I prefer to use “copy” and not “deepcopy”?)

Kind regards

It’s a good question. Someone recently suggested that they should be called copy and shallowcopy. I have noticed that copy is (ever so slightly) faster and allocates less than deepcopy for Float64 array. Which is strange. I would have thought that deepcopy(::Array{Float64}) dispatched to copy.

1 Like

There is an old Julia issue somewhere where Jeff and Tim Holy agree that deepcopy is sort of a lower level function that should rarely be used. I didn’t understand the arguments, but if they agreed, I agree :blush: . (couldn’t find the issue right now).

Found it: Should `copy` be renamed `shallowcopy`? · Issue #42796 · JuliaLang/julia · GitHub

1 Like

I think I get it now. Using copy you keep the references, but actual values are not referenced? Like this example:

a = [1.0;2;3;4]

b = copy(a)

a[1] = 5

# We observe that b[1] is still 1
println(b)

Actually I still don’t understand it :slight_smile:

Kind regards

The difference is if the elements of the container are mutable:

julia> x = [ [1,2], [3,4] ] # array of arrays
2-element Vector{Vector{Int64}}:
 [1, 2]
 [3, 4]

julia> y = deepcopy(x);

julia> y[1][1] = 0
0

julia> x # is preserved
2-element Vector{Vector{Int64}}:
 [1, 2]
 [3, 4]

julia> y = copy(x);

julia> y[1][1] = 0
0

julia> x # now it changed
2-element Vector{Vector{Int64}}:
 [0, 2]
 [3, 4]

1 Like

Okay I see, thanks!

For my use cases I always want to cut the connections, so I think I will stick to deepcopy always - copy makes me a bit uneasy, since I cannot figure out when it is useful to have the same underlying object with two names.

Kind regards

In “flat code” it is not very useful, probably. But that is consistent with what happens on function calls, in which mutable structure are not copied (which would imply possibly huge allocations in every call):

julia> f(x) = x .= .+ 1 # mutates x
f (generic function with 1 method)

julia> a = zeros(3);

julia> f(a);

julia> a
3-element Vector{Float64}:
 1.0
 1.0
 1.0

Thus, for a mutable structure, the label assigned to it is just a label. And some care has to be taken with that.

The same happens if you share a big array within two structures:

julia> struct A
           x::Vector{Float64}
       end

julia> struct B
           x::Vector{Float64}
       end

julia> a = A(rand(3)); # could be very large

julia> b = B(a.x);

julia> a.x
3-element Vector{Float64}:
 0.008402191874595566
 0.027938927073541286
 0.8813538319613927

julia> a.x[1] = 0.
0.0

julia> b.x
3-element Vector{Float64}:
 0.0
 0.027938927073541286
 0.8813538319613927

This is about being efficient in memory management.