Can a method push! to a Vector that is a field in a struct?

In https://github.com/dmbates/Wordlegames.jl I use a struct to represent the state of a Wordle (or similar) game. I am thinking of recording the history of guesses and scores from these guess as fields which will be Vector{<:Integer}. (In Wordle “Hard Mode” the guess must always be from the current target pool and it can be represented as an index into the initial target pool. Similarly a score can be regarded as a number in base-3.)

Because Wordle allows only 6 guesses, the vectors could be fixed-length. But I would prefer to avoid a restriction like that in the design, because I want to analyze related games that may need more than 6 guesses. Also, it would be convenient to empty! this container at the beginning of the game and push! results onto it so it is easy to determine things like what the last guess was.

If I declare the field to be, say, Vector{Int} and initialize it to, say, sizehint!(Int[], 10), can I safely push! to it even though it is a field in a struct. The situation I am trying to avoid is push! causing a re-allocate and copy when the available storage for the vector is exhausted. It is fine to change the contents of a field in a struct but not the location.

If the question is whether you can mutate fields of an immutable struct, then the answer is yes, if their type is mutable, that’s totally fine:

julia> struct Foo
           x::Vector{Int}
           y::Int
       end

julia> a = Foo(Int[], 2)
Foo(Int64[], 2)

julia> push!(a.x, 42)
1-element Vector{Int64}:
 42

julia> a
Foo([42], 2)

What you can’t do is to rebind the fields to something else with the assignment operation:

julia> a.x = [17]
ERROR: setfield!: immutable struct of type Foo cannot be changed
Stacktrace:
 [1] setproperty!(x::Foo, f::Symbol, v::Vector{Int64})
   @ Base ./Base.jl:43
 [2] top-level scope
   @ REPL[5]:1

julia> a.y = 5
ERROR: setfield!: immutable struct of type Foo cannot be changed
Stacktrace:
 [1] setproperty!(x::Foo, f::Symbol, v::Int64)
   @ Base ./Base.jl:43
 [2] top-level scope
   @ REPL[6]:1

You have to go through in-place operations, like empty! and then push!/append!, for the fields which are mutable, in this case only x:

julia> empty!(a.x)
Int64[]

julia> push!(a.x, 17)
1-element Vector{Int64}:
 17

julia> a
Foo([17], 2)

This is also mentioned in the documentation of types

An immutable object might contain mutable objects, such as arrays, as fields. Those contained objects will remain mutable; only the fields of the immutable object itself cannot be changed to point to different objects.

3 Likes

Thanks for the reply. I may not have phrased it well but I realize that I can change the contents of a field but I can’t rebind it. What I was asking specifically is what happens if push! causes the rebinding behind the scenes?

As I understand it, when using a Vector as a stack or a deque the memory for the vector is over-allocated initially so that elements can be pushed on to the end. The sizehint! function allows the programmer to give hints on the initial size. But if you try to push! beyond the allocated memory the operation has to allocate a larger chunk and copy the current contents into the new location. At least, that is how I understand it works.

So there could be a situation where several push! calls are benign but then one comes along that causes a reallocation and hence a rebinding. What happens in that situation?

1 Like

I think the confusion is that the Julia-level Vector is not rebound when push! does something (IIUC). In other words, push! might re-allocate a new contiguous block of memory, but that only changes its internal pointer to that memory, not the Julia-level binding to your Vector.

6 Likes

One might answer your question with a question

What happened when you tried it?

struct Foo
v
end

f = Foo(Int[])

for i in 1:1_000_000
    push!(f.v, i)
end

2 Likes

I was planning to try it but I thought I would ask here first in case someone knew the answer immediately.

It turns out that my idea of how a Vector is implemented was wrong. I hadn’t realized that there was the extra level of indirection that @palday mentioned.