1

this is an odd problem I have been having. Essentially, I am inadvertently changing the value of a field of an immutable struct and I have no idea how! I have created a reproducible problem, although it is a bit long, so please bear with.

Suppose we have a vector of points, and we wish to swap the first element of the vector with a new point. We create a swap_first type which stores the vector of original points and the new point.

struct Point
    x::Float64
    y::Float64
end

struct swap_first
    orig_points::Vector{Point}
    new_point::Point
end

Then, creating some points and a function which swaps the first point with the new point:

p1=Point(1,2);
p2=Point(3,4);
orig=[p1,p2];

newp=Point(10,11);


swap_object=swap_first(orig,newp);

function func_swap(val::swap_first)
    new_p=val.orig_points;
    new_p[1]=val.new_point;
    return new_p
end

func_swap(swap_object);

And somehow, applying this function which creates a new vector of points without touching the original object, we have changed the value of the field orig_points of the variable swap_object.

println(swap_object.orig_points)
Point[Point(10.0, 11.0), Point(3.0, 4.0)]

The original elements of the vector were [Point(1,2),Point(3,4)]. I want to keep the original value of swap_object, as well as have the new vector of points! I thought changing fields of an immmutable struct wasn't possible because of the immutability of structs! Help on this would be greatly appreciated. Thanks for reading.

  • 1
    Possible duplicate of: https://stackoverflow.com/q/50162935/1346276, and probably some other related questions... – phipsgabler Jul 14 '22 at 11:49
  • 1
    And for your final ask of "I want to keep the original value of swap_object, as well as have the new vector of points!", see https://stackoverflow.com/questions/35115414/copy-or-clone-a-collection-in-julia. And Stefan Karpinski's remarks on **Assignment** vs **Mutation** in this answer https://stackoverflow.com/a/33003055/8127 may clarify some things too. – Sundar R Jul 14 '22 at 12:06
  • In short this is possible because in _immutable_ structs you can not reassign a field to a different object (ie change the memory address to which that field name points to), but if the objedt is itself mutable, this can mutate, with the memory address referenced by the name remaining the same. – Antonello Jul 15 '22 at 21:26

2 Answers2

0

Thanks to the useful comments, the answers are that a) although the struct itself is immutable, the fields themselves aren't. This seems like a subtle point. And b) one must copy the original mutable field as to not change the original variable, so just writing new_p=copy(val.orig_points); in the function instead is sufficient to solving the problem!

  • 1
    Not quite, see my answer for more details. a) the fields of a struct are immutable. But arrays are references, which means that their content can change. b) beware that if the content of arrays are other references, you may want to make a deepcopy instead of a simple copy. See https://discourse.julialang.org/t/what-is-the-difference-between-copy-and-deepcopy/3918 for a discussion on the differences between the two. – Amon Jul 15 '22 at 12:53
0

You need to make the distinction between your struct which is indeed immutable, and your array of struct which is mutable.

julia> struct Point
           x::Float64
           y::Float64
       end

julia> p = Point(1,2)
Point(1.0, 2.0)

julia> p.x = 3
ERROR: setfield! immutable struct of type Point cannot be changed
Stacktrace:
 [1] setproperty!(x::Point, f::Symbol, v::Int64)
   @ Base .\Base.jl:34
 [2] top-level scope
   @ REPL[38]:1

Your struct Point is immutable and you can't change it even if you try. However, your struct swap_first contains an array. The reference to the array cannot change, but the content of the array can change.

When you type:

new_p=val.orig_points;

You are not creating a new array. You are just creating a new reference to the array val.orig_points. This means that when you modify the content of new_p you are also modifying the content of val.orig_points, simply because they are the same.

What you want is to copy your array first, like this:

function func_swap(val::swap_first)
    new_p=copy(val.orig_points); # modification here
    new_p[1]=val.new_point;
    return new_p
end

That way new_p and val.orig_points will point to different content, and you'll be able to modify one without touching the other, resulting in the following behaviour:

julia> func_swap(swap_object)
2-element Vector{Point}:
 Point(10.0, 11.0)
 Point(3.0, 4.0)

func_swap will return a modified object, but your original object remains unchanged:

julia> @show swap_object.orig_points
swap_object.orig_points = Point[Point(1.0, 2.0), Point(3.0, 4.0)]
2-element Vector{Point}:
 Point(1.0, 2.0)
 Point(3.0, 4.0)
Amon
  • 402
  • 2
  • 8