Ambiguity in change of variable behaviour in a struct?

I was recently writing some code and noticed a peculiar behaviour of structs

Notice how earlier, I define a variable a with value 1. I push this into a list. Change the value of the list variable but that doesn’t change ‘a’.

However, if I define a struct. And NOW push it into a list, and alter the list element. It changes the struct!
What’s happening? Is it normal and preferred behaviour or an artifact of the mutable type or @kwdef argument?

That behavior is expected, the manual goes over this behavior in the section Variables · The Julia Language.

In the case of the mutable struct and a = Hello(5). You are assigning the first index of the list to be bound to the variable a. At which point you are mutating the variable bound to a, and not assigning a new value to the first index of the list.

3 Likes

The proper comparison of what you do with the mutable struct is what happens to your list in the first case. Note that the list element is modified, as the element of the struct, without changing the value bound to the label a:

julia> a = 1
1

julia> list = []
Any[]

julia> push!(list, 1)
1-element Vector{Any}:
 1

julia> list[1] = 5
5

julia> list
1-element Vector{Any}:
 5

julia> a
1

The above is equivalent, in this sense, to:

julia> mutable struct A
           x
       end

julia> s = A(a)
A(1)

julia> s.x = 5
5

julia> s
A(5)

julia> a
1

where the struct is playing the same role as the list, as being a mutable container of a data value.

Ahhh! Any specific reasons for that? Ig that’s the opposite of how normal variables work when pushed into a list.

Also, if I don’t want this behaviour, how would I define the list? Perhaps push a .copy of the struct?

I’m sorry I did not catch that…

There aren’t “normal” or “not normal” variables. There are mutable values and immutable values.
The mutable struct is mutable. Thus you can mutate it, for example by changing list[1].val = 5 as in your example. There, list[1] is a mutable struct of type Hello, and you are mutating the value of val inside it. The list itself is also a mutable type, thus why you can replace the values of its elements.

In your original example, you are mutating the value val of the mutable struct that is stored in the first element of list.

And, indeed, if you want to mutate the instance that is stored in list[1] without affecting it outside, you need to copy it. That is the same as:

julia> x = [1]
1-element Vector{Int64}:
 1

julia> list = [ x ]
1-element Vector{Vector{Int64}}:
 [1]

julia> list[1][1] = 0
0

julia> x
1-element Vector{Int64}:
 0

julia> list = [copy(x)]
1-element Vector{Vector{Int64}}:
 [0]

julia> list[1][1] = 2
2

julia> x
1-element Vector{Int64}:
 0

Where x is taking the role of your mutable struct.

1 Like

Yeah, I understand mutable structs are mutable. So is the variable ‘a’ though. My question was, when pushed into a list, why is a new element indexed in a list that would now be unrelated to the root variable ‘a’. Whereas in case of a mutable struct, the pushed value is bound to the struct, not assigning a new value.

@Daniel_Berge did point out this is how struct behaviour is defined

No, no, the a is just a label to a value. The value when a = 1 is 1 an that is an immutable value. If you push 1 to a list, the label a is still bound to the value 1. If you change the value of the list to something else (like 5), that does not affect the value to which the label a is bound.

This is the same with a mutable value:

julia> x = [1];

julia> list = [x]
1-element Vector{Vector{Int64}}:
 [1]

julia> list[1] = [2] # bind list[1] to *another* value
1-element Vector{Int64}:
 2

So, in this sense, they are exactly the same. The difference is that if you do, above:

julia> list[1][1] = 5
5

julia> list
1-element Vector{Vector{Int64}}:
 [5]

you are mutating list[1], not assigning it to another value (mutable or not), as in the first case.

2 Likes

image
Ahhhhh now I get your point. Woah! I didn’t know that.

So any structure that inherently has its own elements… like a list, or a struct would be mutable? When pushed into a list, all variables are bound in their original form. When you redefine an element of the list, you just disconnect the variable. But if you mutate an element of the list’s element, does that mutate the actual struct that was bound in the first place?

That’s what I understood. Is that correct?

No, not really. A struct can be immutable as well, then you simply cannot do that:

julia> struct A
           x
       end

julia> a = A(1)
A(1)

julia> list = [a]
1-element Vector{A}:
 A(1)

julia> list[1].x = 1
ERROR: setfield!: immutable struct of type A cannot be changed

To change list[1] you need to replace the structure completely (identically to if it was a number):

julia> list[1] = A(2)
A(2)

julia> list
1-element Vector{A}:
 A(2)

All right, I have to admit… I did not know that! Woah! a = 2 followed by a = 3 does not change the value of a. But rather makes a point to a different element!

Gotta admit, never saw it this way in my years of programming. Thank you! (love the Julia documentation lol)

Also, you were right lol… thank you! Just read the Julia documentation!

Note that this is the behavior in Python and most (all?) other interactive languages. The language where that can be really thought differently (as a being the container of the value, rather than simply a label given to a value, is Fortran - on the surface everything in Fortran looks like mutable objects).

ahaha I did check out the same on Python actually. Which is why I said I did not know this for all these years. Lol, Thank you for your help!