Trying to reference a struct field into an array

I’m trying to create a vector as, that contains the value of field a for each struct Foo in the vector of structs foo without losing reference so that changing the values of as would also affect the values of foo. Here is a simple example:

julia> mutable struct Foo{N<:Number}
           a::N
           b::N
       end

julia> foo = [Foo(1, 1), Foo(2, 2)]
2-element Vector{Foo{Int64}}:
 Foo{Int64}(1, 1)
 Foo{Int64}(2, 2)

julia> as = [f.a for f in foo]
2-element Vector{Int64}:
 1
 2

julia> as[1] = 3
3

julia> foo[1]
Foo{Int64}(1, 1) ## my target here is to have Foo{Int64}(3, 1) as result instead

well, since numbers are immutable, when you have f.a, you lost reference to struct because values are just values.

either you keep foo around and do foo[1].a = 3, or you have to put Refs inside the struct Foo instead of plain numbers

2 Likes

@jling is right — doing exactly what you ask for is impossible so long as the struct fields are immutable. But what you ask for is the default behavior for mutable fields (such as arrays or a number in a Ref), in which case the struct itself need not be mutable.

I just wanted to add, maybe you will find StructArrays.jl useful. It lets you deal with arrays-of-structs, while internally being a struct-of-arrays. Used in conjunction with Accessors.jl, this can be pretty handy. See this answer.

2 Likes

For this specific usecase, you only need StructArrays. And the Foo struct doesn’t even have to be mutable.

julia> foo = [Foo(1, 1), Foo(2, 2)] |> StructArray
2-element StructArray(::Vector{Int64}, ::Vector{Int64}) with eltype Foo{Int64}:
 Foo{Int64}(1, 1)
 Foo{Int64}(2, 2)

julia> as = foo.a
2-element Vector{Int64}:
 1
 2

julia> as[1] = 3
3

julia> foo[1]
Foo{Int64}(3, 1)
4 Likes

thanks a lot for suggesting StructArrays. Any ideas how to customise StructArray in case of nested structs?, something like following:

using StructArrays

julia> struct SubFoo{N<:Number}
           x::N
           y::N
           z::N
       end

julia> struct Foo{N<:Number}
          a::SubFoo{N}
          b::N
       end

julia> foo = [Foo(SubFoo(1, 2, 3), 1), Foo(SubFoo(1, 2, 3), 2)] |> StructArray
2-element StructArray(::Vector{SubFoo{Int64}}, ::Vector{Int64}) with eltype Foo{Int64}:
 Foo{Int64}(SubFoo{Int64}(1, 2, 3), 1)
 Foo{Int64}(SubFoo{Int64}(1, 2, 3), 2)

julia> foo.a.x ## not possible, would be nice to get [1, 1] as a result

Storing Foo.a as StructVector{SubFoo...}?..

This part of StructArrays is less known, but works fine:

julia> foo = StructArray([Foo(SubFoo(1, 2, 3), 1), Foo(SubFoo(1, 2, 3), 2)]; unwrap=T -> fieldcount(T) > 0)
2-element StructArray(StructArray(::Vector{Int64}, ::Vector{Int64}, ::Vector{Int64}), ::Vector{Int64}) with eltype Foo{Int64}:
 Foo{Int64}(SubFoo{Int64}(1, 2, 3), 1)
 Foo{Int64}(SubFoo{Int64}(1, 2, 3), 2)

julia> foo.a.x
2-element Vector{Int64}:
 1
 1

And just in case you need something even fancier:

julia> using AccessorsExtra, SplitApplyCombine

julia> ax = mapview(@optic(_.a.x + 10), foo)
2-element <...>:
 11
 11

julia> ax[1] = 0
0

julia> foo
2-element <...>:
 Foo{Int64}(SubFoo{Int64}(-10, 2, 3), 1)
 Foo{Int64}(SubFoo{Int64}(1, 2, 3), 2)

Works with all arrays including Base Vectors.

1 Like

thanks, the first solution works perfectly for me. I’ve just gone with T <: SubFoo instead of fieldcount(T) > 0, as fieldcount wouldn’t work with abstract types or Unions.