I get a StackOverflowError
when I try to build a struct object from another, previously existing one in which one of the parameters depends on the others.
I define a type like the following:
using Parameters
julia> @with_kw struct A
a1 = 0
a2 = 0
b = (; b1 = a1, b2 = a2)
end
From this I can build an object A
:
julia> anobject = A(; a1 = 1, a2 = 3)
having:
julia> anobject.b = (; b1 = 1, b2 = 3)
Now, I need to build a second object from the first one, updating some values:
julia> object2 = A(anobject; a1 = 5)
but this results in:
julia> object2.a1 = 5
julia> object2.a2 = 3
julia> object2.b = (; b1 = 1, b2 = 3)
b
has not been updated.
I have tried to define a constructor:
julia> A(a::A; a1 = 0, kw...) = A(a; a1 = a1, b = (; a.b..., b1 = a1), kw...)
but when using it, it results in a StackOverflowError
.
julia> object3 = A(anobject; a1 = 5)
ERROR: StackOverflowError:
which of course I do not understand where it is coming from nor have I found any relevant documentation. What is the proper method to do this?
Your A
constructor is recursively calling itself instead of calling the default A
constructor with keywords. The stack trace frame [4] says something like the call "(repeats 21760 times)". So it represents 21760 stack frames. Those 21760 repeated stack frames are collapsed into frame [4] to simplify the stacktrace.
ERROR: StackOverflowError:
Stacktrace:
[1] merge_fallback(a::NamedTuple, b::NamedTuple, an::Tuple{Vararg{Symbol}}, bn::Tuple{Vararg{Symbol}})
@ Base .\namedtuple.jl:296
[2] merge
@ .\namedtuple.jl:331 [inlined]
[3] merge
@ .\namedtuple.jl:339 [inlined]
[4] A(a::A; a1::Int64, kw::@Kwargs{b::@NamedTuple{b1::Int64, b2::Int64}}) (repeats 21760 times)
@ Main .\REPL[6]:1
[5] A(a::A; a1::Int64, kw::@Kwargs{})
@ Main .\REPL[6]:1
A quick change is to call the default A
constructor with keywords:
import Parameters: @with_kw
@with_kw struct A
a1 = 0
a2 = 0
b = (; b1 = a1, b2 = a2)
end
A(a::A; a1 = 0) = A(a1 = a1, a2 = a.a2, b = (; a.b..., b1 = a1))
obj1 = A(; a1 = 1, a2 = 3)
# A
# a1: Int64 1
# a2: Int64 3
# b: @NamedTuple{b1::Int64, b2::Int64}
obj1.b
# (b1 = 1, b2 = 3)
obj2 = A(obj1; a1 = 5)
# A
# a1: Int64 5
# a2: Int64 3
# b: @NamedTuple{b1::Int64, b2::Int64}
obj2.b
# (b1 = 5, b2 = 3)
A complex change is to use a package like NamedTupleTools
to convert the old struct into a NamedTuple
, merge that with the new keyword parameters, and then call the default A
constructor with those keywords.
import Parameters: @with_kw
import NamedTupleTools: merge, ntfromstruct
@with_kw struct A
a1 = 0
a2 = 0
b = (; b1 = a1, b2 = a2)
end
function A(a::A; kw...)
a_nt = merge(ntfromstruct(a), NamedTuple(kw))
b_nt = (; b1 = a_nt.a1, b2 = a_nt.a2)
A(; merge(a_nt, (; b = b_nt))...)
end
obj1 = A(; a1 = 1, a2 = 3)
# A
# a1: Int64 1
# a2: Int64 3
# b: @NamedTuple{b1::Int64, b2::Int64}
obj1.b
# (b1 = 1, b2 = 3)
obj2 = A(obj1; a1 = 5)
# A
# a1: Int64 5
# a2: Int64 3
# b: @NamedTuple{b1::Int64, b2::Int64}
obj2.b
# (b1 = 5, b2 = 3)