Define struct programmatically in Julia

Viewed 510

The issue

I read data from a file that is meant to be attached to a tree-like structure. Each line of the file contains attributes about a node of the tree.

Three attributes are mandatory (and are immutable), and the user can give any other attribute. The attributes given by the user can be anything, but we know their name and their type at import because they are given in the file.

When the tree is complete, in a typical use case some fields of the nodes are only read, but some are modified.

I am currently using a Dict{String,Any} to store the user-defined data in my node struct, but I know it is not advisable for performance. This is especially important to consider because a lot of computations are made afterward.

My current struct has two fields, one for the mandatory attributes related to any nodes (they are immutable), and one for further attributes given in the file. The struct looks like this:

mutable struct Node1
    attr1::Node2
    attr2
end

The immutable struct from the first field of Node1 looks like the following:

struct Node2
    link::AbstractString
    symbol::Union{Missing,AbstractString}
    index::Union{Missing,Integer}
end

The question

Now here is my question: can I do any better ?

For example, could I make programmatically my Node1 struct with fields based on the attributes given in the file ? That would allow me to at least give the type of each attribute instead of just Any.

1 Answers

You can use parametric types for this. The idea is that the type of the field attr2 is determined at runtime when you create your node object. Parametric types allow the type of a field to be variable. Here's an example of what this would look like:

struct ImmutableNodeData
    link::String
    symbol::Union{Nothing,String}
    index::Union{Nothing,Int}
end

mutable struct Node{T}
    attr1::ImmutableNodeData
    attr2::T
end

I've renamed the structs to be a little more informative. Here's how we can use these types at the REPL:

julia> imm = ImmutableNodeData("a", "b", 1)
ImmutableNodeData("a", "b", 1)

julia> n1 = Node(imm, 100)
Node{Int64}(ImmutableNodeData("a", "b", 1), 100)

julia> n2 = Node(imm, "hello")
Node{String}(ImmutableNodeData("a", "b", 1), "hello")

Note how we can create a Node{Int64} or a Node{String} just by calling the constructor with the second argument being an Int64 or a String, respectively.

Also note that for ImmutableNodeData I've changed the types of the fields to be either concrete types or unions of concrete types. For performance, you always want the fields of your structs to have concrete types rather than abstract types.

EDIT:

If the attr2 name is also determined at runtime, then you could split attr2::T into two fields like this:

mutable struct Node{T}
    attr1::ImmutableNodeData
    attr_name::String
    attr_val::T
end

EDIT #2:

If there are multiple user provided attributes, then you could store all of those attributes in a NamedTuple, as Oscar Smith mentioned in a comment. And you can include this named tuple in your Node struct as a field with a parametric type, like this:

mutable struct Node{T <: NamedTuple}
    attr1::ImmutableNodeData
    user_attrs::T
end

However, if you want those to also be mutable, then perhaps you could use a MutableNamedTuple instead of a NamedTuple.

EDIT #3:

In response to a question in the comments:

The advantage of NamedTuple and MutableNamedTuple over Dict is that they allow the type of each field to be different, and the types of the fields are included in the type signature of the named tuple. For example, compare the types of the following named tuple and dictionary, which hold basically the same data:

julia> typeof((a=1, b="z"))
NamedTuple{(:a, :b),Tuple{Int64,String}}

julia> typeof(Dict(:a => 1, :b => "z"))
Dict{Symbol,Any}

Note that the named tuple stores the types of both fields (Int64 and String), whereas the dictionary just treats the values as having type Any.

I suppose it is faster for read operations but slower for mutation right ?

I wouldn't expect mutating a MutableNamedTuple to be slower than mutating a Dict, but you'd have to do some benchmarking to confirm that.

Related