Proper use of recursive fields in structs / types

I am writing my first module and have several data structures that will be all linked to each other, sort of like blog posts and comments. I have a file for each data structure that will house methods relevant to each. The following code doesn’t work as the compiler doesn’t know what a Comment data type is when it parses the Post struct.

File Blog.jl

module Blog
include("post.jl")
include("comment.jl")
end

File post.jl

struct Post
    comments::Vector{Comment}
end

File comment.jl

struct Comment
    post::Post
end

Since the include command is a simple copy paster, adding include statements in every file doesn’t work either, as it creates an infinite loop.

What is the recommended way of doing this?

2 Likes

It seems to me that the problem is the definition of the types.
There is no place to stop when interpreting the types: it is recursive all the way down to the turtles.

So what is the most effective way of implementing this design pattern in Julia? I’ve played around a bit and here is what I found:

Undef: Post not defined

struct Comment
    post::Post
end

struct Post
    comments::Vector{Comment}
end

but if you use generics, then it works

struct Post{C <: Vector}
    comments::C
end

struct Comment
    post::Post
end

I’ve even thought of a completely different design pattern like this:

struct Post
   author_id::Int
   text::String
end

struct Comment
   user_id::Int
   text::String
end

struct PostComments
   post::Post
   comments::Vector{Comment}
end

Perhaps this is the preferred way to implement this? But it would be so inefficient without any way of being able to query the parent post like so my_comment.post.

So you want the comments to contain references to posts, not necessarily the post themselves. Correct?

that’s correct, I want a pointer to the Post object (not a duplicate)

I also found this answer on SO from 2019 that seems to cover this exact question.

abstract type AbstractComment end
abstract type AbstractPost end

struct Comment{P <: AbstractPost} <: AbstractComment
    post::P
end

struct Post{C <: AbstractComment} <: AbstractPost
    comments::Vector{C}
end

Thanks. Can you remove the use of abstract comment and use the actual Comment type since it gets defined first? Compiler only needed one of them in my test.

And tangential question - where do I instantiate the empty Comment[] array? Pass it to the default constructor? I wasn’t able to do something Iike C[] in an inner constructor.

julia> abstract type AbstractComment end

julia> abstract type AbstractPost end

julia> struct Comment{P <: AbstractPost} <: AbstractComment
           post::P
       end

julia> struct Post{C <: AbstractComment} <: AbstractPost
           comments::Vector{C}
           Post(comments::Vector{C} = Comment[]) where C <: AbstractComment = new{C}(comments)
       end

julia> Post()
Post{Comment}(Comment[])

julia> Post(AbstractComment[])
Post{AbstractComment}(AbstractComment[])

Yes, but now we’re not following the design pattern.

abstract type AbstractPost end

struct Comment{P <: AbstractPost}
    post::P
end

struct Post<: AbstractPost
   comments::Vector{Comment}
end
Post() = Post(Comment[])