How to make mutually referencial structs?

Hello, here is a MWE:

mutable struct Bar{T}
    y::T
    foo
end
Bar(y) = Bar(y, nothing)

mutable struct Foo{T, B <: Bar}
    x::T
    bar::B
end

function Foo(x, bar) 
    foo = Foo(x, bar)
    bar.foo = foo
    return foo
end


bar = Bar(2.)
foo = Foo(10,bar)
bar.foo.x

Basically I would like to figure out a way to have two instances, one of each struct, to refer to one another. So, despite the type instability that might ensue, I made Bar with an free typed foo field. When I itinitialize bar, I initialize as nothing and when I instantiate a Foo, it changes the reference of bar.foo from nothing to foo. There’s no error during construction, but the last line returns

ERROR: type Nothing has no field x
Stacktrace:
 [1] getproperty(::Nothing, ::Symbol) at .\Base.jl:33
 [2] top-level scope at REPL[2]:1

So I tried instead to use incomplete initialization

mutable struct Bar{T}
    y::T
    foo
    Bar(y) = new(y)
end

But then I get the error

ERROR: syntax: too few type parameters specified in "new{...}" around REPL[3]:1
Stacktrace:
 [1] top-level scope at REPL[3]:1

I don’t understand, that’s the point of incomplete initialization to have fewer fields…
Note that I can’t know in advance which type parameters bar.foo will have. In fact, I need bar.foo to be able to accept other structs than Foo.

EDIT: There was a y instead of x but that was not the problem.

So I just found that this might be a scope problem ?
because when I set (after the first example) in the global scope

bar.foo = foo
bar.foo.x

correctly returns 10.

So it’s the assignment in Foo’s cosntructor that does not do what I expect.

As it turns out, the recursive type definitions aren’t actually relevant to the problems you’re having.

First off, this function:

isn’t working for you because it’s not being called at all. Try this:

julia> methods(Foo)
# 2 methods for type constructor:
[1] (::Type{Foo})(x::T, bar::B) where {T, B<:Bar} in Main at REPL[3]:2
[2] (::Type{Foo})(x, bar) in Main at REPL[10]:1

Method [1] is the constructor, which matches any x and and bar::Bar. Method [2] is the function you wrote, which matches any x and any bar. So method [2] is less specific and is therefore never actually being called. That’s why it has no effect.

If you make that function more specific (e.g. by making it Foo(x, bar::Bar), then you’re going to run into more problems because on the very next line you do foo = Foo(x, bar). If this function were working at all, it would always just call itself immediately, leading to a stack overflow.

This is a classic case where an inner constructor solves the problem:

julia> mutable struct Foo{T, B <: Bar}
           x::T
           bar::B
           
           function Foo(x::T, bar::B) where {T, B <: Bar}
             foo = new{T, B}(x, bar)
             bar.foo = foo
             return foo
           end
       end

julia> foo = Foo(10, bar)
Foo{Int64,Bar{Float64}}(10, Bar{Float64}(2.0, Foo{Int64,Bar{Float64}}(#= circular reference @-2 =#)))

julia> foo.bar.y
2.0

This isn’t about the incomplete initialization–try it on a simpler case:

julia> struct Baz{T}
         x::T
         
         Baz(x) = new(x)
       end
ERROR: syntax: too few type parameters specified in "new{...}" around REPL[17]:1  

You have to tell new what T should be, otherwise how does it know what type to construct?

julia> struct Baz{T}
         x::T
         
         Baz(x::T) where {T} = new{T}(x)
       end

By the way, you can also make the foo field of Bar concrete in your example using an extra type parameter (although it starts to get a bit mind-bending). Here’s a simplified example of two types which contain each other with no type instabilities:

julia> struct Bar{F}
         foo::F
       end

julia> mutable struct Foo
         bar::Bar{Foo}
         
         function Foo()
           foo = new()
           foo.bar = Bar(foo)
           return foo
         end
       end

julia> foo = Foo()
Foo(Bar{Foo}(Foo(#= circular reference @-2 =#)))

julia> foo.bar
Bar{Foo}(Foo(Bar{Foo}(#= circular reference @-2 =#)))

julia> foo.bar.foo === foo
true
5 Likes

Haa yes okay it makes sense now.

Thank for the concrete type example, but that imposes bar to be constructed within Foo's constructor, no ?

Thank you though.

2 Likes