VarStructs.jl: variable Julia structs

I want to announce the VarStructs.jl package which introduces variables structs with dispatching and redefinition support.

using Pkg; Pkg.add("VarStructs")
using VarStructs

Features

VarStructs are similar to structs but have extra features:

  • You can add fields after their definition.
  • They can be defined inside a function or a local scope.
  • They can be redefined.

Similar to structs

  • They can be used for dispatching (zero-cost)
  • They can have custom constructors
  • They have type conversion/checking for the fields that are declared

Declaration

There are two ways to declare them:

Struct Syntax

In this syntax, providing initial values and types for the fields are optional.

@var struct Animal
    name::String
    number::Int64
end

Call Syntax

In this syntax, you should provide the initial values for the fields. Providing type for the fields are optional (if not provided, it is considered as Any).

@var Person(
        name = "Amin",
        number::Float64 = 20.0,
    )

Getting an Instance

Use the following syntax for getting an instance:

julia> person = Person(name = "Amin", number = 20.0)
Person(
    name::Any = Amin,       
    number::Float64 = 20.0,
)

# Type conversion for the fields that were declared
julia> person2 = Person(name = "Amin", number = 20)  # number is converted to Float64
Person(
    name::Any = Amin,       
    number::Float64 = 20.0,
)

# Type checking for the fields that were declared
julia> person2 = Person(name = "Amin", number = "20")
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Float64

# new field added
julia> person2 = Person(name = "Amin", number = 20.0, initial = "T")
Person(
    initial::String = T,    
    name::Any = Amin,       
    number::Float64 = 20.0,
)

The two syntaxes that are used for declaration also return an instance of the VarStruct. So if you need an instance right away, you can use the following. Note that in redeclaration you will not get type checking based on the previous declaration.

animal = @var Animal(
        name = "lion",
        number::Int64 = 10,
    )

# redefinition of `Animal` returns a new instance:
animal2 = @var Animal(
        name = "dog",
        number::Int64 = 1,
    )

Accessing, Setting, Adding Fields

# Accessing
julia> person.name
"Amin"

# Setting
julia> person.name = "Tim"

# Adding Fields
julia> person.initial = "T"

Dispatch

info function dispatch for Person and Animal type:

function info(x::Person)
    println("Their home is city")
    return x.name
end

function info(x::Animal)
    println("Their home is jungle")
    return x.name
end
julia> info(person)
"Their home is city"
"Amin"

julia> info(animal)
"Their home is jungle"
"lion"

Custom Constructor

To define a custom constructor return an instance using the keyword method:

function Person(name, number)
    return Person(
        name = name,
        number = number,
        initial = name[1],
    )
end

Person("Amin", 20.0)

Give me benchmarks:

The dispatching is zero-cost like a normal struct:

julia> using VarStructs

julia> @var struct A
       end;
julia> @var struct B
       end;

julia> dispacth_varstruct(x::A) = 1
dispath_varstruct (generic function with 1 method)

julia> dispacth_varstruct(x::B) = 2
dispath_varstruct (generic function with 2 methods)

julia> using BenchmarkTools
julia> @btime dispath_varstruct(x) setup=(x=rand()<0.5 ? A() : B())
  0.001 ns (0 allocations: 0 bytes)
15 Likes

VarStructs 0.3.0 has been released and it has passed its test phase.

The improvements:

  • Unset fields now have a type of Unset instead of Missing
  • Fixed a bug that fields with complex types were not being parsed correctly

I have added a schema example, which shows the power of VarStructs. This example shows how to define a schema for your data and add/process the actual data dynamically.

I’m a little confused by this, it seems like each VarStruct has only one single instance, and trying to create a new instance just mutates the old one?

julia> using VarStructs

julia> @var struct Foo
           a::Int
       end;

julia> f = Foo(a=1)
Foo(
    a::Int64 = 1, 
)

julia> g = Foo(a=2, b=1)
Foo(
    a::Int64 = 2, 
    b::Int64 = 1, 
)

julia> f
Foo(
    a::Int64 = 2, 
    b::Int64 = 1, 
)

Is this intentional? If so, I don’t really think it’s appropriate to call these things structs…

Even more bizarre:

julia> f == g
false

julia> f === g
false
2 Likes

Yes, this is confusing a bit. I need to make the non-shared instances the default behavior instead. Shared instances probably need to be annotated in the declaration explicitly.

I will consider adding this feature in the next version.

VarStructs 0.4.0 is released with support for unique instances (like normal structs).

Now, @var will create VarStructs that will construct unique instances. Use @shared_var for shared VarStructs that construct shared instances.

2 Likes