Setfield: set properties of nested subfields

I’m trying to use the setproperties function of Setfield.jl to set multiple different fields at once for immutable structs, but I’m running into this issue.

using Setfield

struct A
    foo::Int
    bar::Int
end

struct B
    baz::A
end

y = A(1, 2)
x = B(A(1, 2))

y = setproperties(y, (foo = 2, bar = 1))
x = setproperties(x, (baz.foo = 2, baz.bar = 1))

The second to last line works, but the last line has this error

syntax: invalid named tuple field name “baz.foo”

Stacktrace:
[1] top-level scope at …/.julia/packages/IJulia/DrVMH/src/kernel.jl:52

I know I can use @set to do this, but I’m trying to limit the number of times the struct is copied. Is there any way to do this using Setfield.jl or a similar tool?

Looking at
help?> setproperties
using it in the nested case is not supported, which makes sense to me.

The patch::NamedTuple argument is meant to be using parts of the properties of the struct.
So it should be e.g.

x = setproperties(x, (baz = setproperties(y, (foo = 2, bar = 1)) ))

if you like this (I don’t). Or
x = setproperties(x, (baz = A(2,1) ))

By the way, your examples do not need setproperties. They are just the same as setting a new y overwritting the old one:

y = A(2,1)
y = A(1,2)

It would make more sense if these are the examples:

y = A(1, 2)
y = setproperties(y, (foo = 2))

and

x = B(A(1, 2))
x = setproperties(x, (baz = setproperties(y, (foo = 2)) ))

now using setproperties here is a good MWE for larger structs with many properties.

setproperties does not reduce the number of copies. It is a convenience function for large structs where only small number of properties are changing when copied.

1 Like

Thanks! If I do

using Setfield

struct A
    foo::Int
    bar::Int
end

struct B
    baz::A
end

x = B(A(1, 2))

function with_set_properties(x) 
    return setproperties(x, (baz = setproperties(x.baz, foo = 2, bar = 1)))
end
function without_set_properties(x)
    return @set (@set x.baz.foo = 2).baz.bar = 1
end
@time y = with_set_properties(x)
@time z = without_set_properties(x);

It seems the former is more performant

  0.006966 seconds (19.32 k allocations: 1.048 MiB)
  0.010215 seconds (29.68 k allocations: 1.623 MiB)

and does fewer allocations (which I suspect is where the performance comes from).

Yeah, my real use case for this is much more complicated, I was just trying to make as simple an example as possible that involved changing multiple, nested fields

1 Like

I don’t want to dive too deep into the issue but if I check performance with BenchmarkTools it is different:

julia> using BenchmarkTools

julia> @benchmark with_set_properties($x)
BenchmarkTools.Trial: 
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     1.399 ns (0.00% GC)
  median time:      1.500 ns (0.00% GC)
  mean time:        1.516 ns (0.00% GC)
  maximum time:     4.200 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     1000

julia> @benchmark without_set_properties($x)
BenchmarkTools.Trial:
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     1.199 ns (0.00% GC)
  median time:      1.201 ns (0.00% GC)
  mean time:        1.251 ns (0.00% GC)
  maximum time:     17.300 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     1000

But the difference is so small that it is perhaps just an artefact.

You may search the discussions for allocations and benchmarking. I remember there are some good explanations about this, which may help in understanding whats going on.

1 Like

Just confirming that on my machine, BenchmarkTools does indeed show this is probably within random error.

BenchmarkTools.Trial: 
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     1.179 ns (0.00% GC)
  median time:      1.200 ns (0.00% GC)
  mean time:        1.199 ns (0.00% GC)
  maximum time:     8.540 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     1000

BenchmarkTools.Trial: 
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     1.189 ns (0.00% GC)
  median time:      1.200 ns (0.00% GC)
  mean time:        1.216 ns (0.00% GC)
  maximum time:     26.410 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     1000

Not really sure why Benchmark tools is showing no allocations here though