Package Parameters, mutable struct as function argument

I am using the Parameters package to do some work. One of the parameters is an array and surprisingly the struct gets changed when I give it as an argument to a function that modify the array after unpacking

using Parameters

@with_kw struct MyStruct
    x::Array{Float64,1} = zeros(N)
end

function noModify(structIn::MyStruct)
    @unpack x = structIn
    x = ones(N)
end

function yesModify(structIn::MyStruct)
    @unpack x = structIn
    for i=1:N
        x[i] = 2.0
    end
end

N = 10
exampleStruct = MyStruct()

println(exampleStruct.x)
noModify(exampleStruct)
println(exampleStruct.x)
yesModify(exampleStruct)
println(exampleStruct.x)

@unpack x = exampleStruct
x = 3 * ones(N)
println(exampleStruct.x)
for i=1:N
    x[i] = 4.0
end
println(exampleStruct.x)

It runs as

[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
[2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0]
[2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0]
[2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0]

As expected the function noModify doesn’t modify the global variable exampleStruct.
What I found weird is that the function yesModify does!
Of course doing it directly in the global scope doesn’t change anything. The only case that is unexpected for me is the one in the function yesModify that has a for loop inside.
Am I missing something? is that something that is supposed to happen? I thought that even if I unpack something and modify it, the orignal structure (called exampleStruct in this case) should not change.

This is just how Julia works. Unpacking scope, and structs are all just distractions here–the core of the matter is:

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

julia> b = a
3-element Array{Int64,1}:
 1
 2
 3

At this point, b and a are labels for the same array. Modifying an entry of b modifies a as well because they are labels of the same value:

julia> b === a
true

julia> b[1] = 5
5

julia> a
3-element Array{Int64,1}:
 5
 2
 3

However, if you do:

b = ones(3)

Then you have created a new array of ones and then attached the b label to that new array. b and a are now completely unrelated, and your change to b has no effect on a:

julia> b === a
false

julia> a
3-element Array{Int64,1}:
 5
 2
 3

@unpack is just a helpful macro to produce syntax like x = structIn.x. That means that x is just a label attached to the same data as structIn.x. Modifying an entry of x must then modify the data in structIn because they are still the same array. Doing x = ones(N) creates a new array and attaches the x label to that array. This now has nothing to do with structIn's data, so it is not modified.

4 Likes

Just one point to add in addition to @rdeits response…

The only reason you’re not seeing the modification in your global scope code:

for i=1:N
    x[i] = 4.0
end

is because the x = 3 * ones(N) call just before reassigned x to not be an alias for the exampleStruct.x. Whether in global of function scope, @rdeits explanation above is valid.

2 Likes

Thanks @rdeits, @mchitre! Now it’s more clear