Search code examples
structtypesconstructorjuliastack-overflow

StackOverflowError when constucting a struct from another in Julia


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?


Solution

  • 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)