How to make mutually referencial structs?

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
6 Likes