[Ann] MixedStructTypes.jl - Pack heterogeneous types in a single one

I’m happy to announce MixedStructTypes.jl which allows to compactify multiple mutable and immutable types in a single one with a similar syntax to the one of structs. They work identical to structs for many operations because many Base functions are implemented to match the interface for normal structs. The point of working with a unique type instead of many is to avoid dynamic dispatch and abstract containers which have big performance hits.

Two different macros are available, having different memory and speed performance characteristics. The macro based on SumTypes.jl is also a bit more general because it allows to mix mutable and immutable structs where fields belonging to different structs can also have different types. Nonetheless, both already can contain parametric types and allow default values for fields. I think that the fact that the syntax is so similar to the one of structs should help integrate this package into other ones.

An example of usage and a little performance comparison between the two macros are already present in the ReadMe.

Let’s also explore here how do these two macros compare with the one from Unityper.jl. While these macros have also more features than Unityper because they allow for parametric mutable and immutable structs, while Unityper only allows for non-parametric immutable structs, they are also good performance-wise, to show that I repeat the benchmark on the ReadMe but with fewer types for the sake of brevity:

using MixedStructTypes, Unityper, BenchmarkTools

@compactify begin
    @abstract struct AT end
    struct A <: AT 
        a::Int = 1 
    end
    struct B <: AT 
        a::Int = 2
        b::Complex = 1 + 1im
    end
end

@compact_struct_type @kwdef CT begin
    struct C 
        a::Int = 1 
    end
    struct D 
        a::Int = 2
        b::Complex = 1 + 1im 
    end
end

@sum_struct_type @kwdef ET begin
    struct E 
        a::Int = 1 
    end
    struct F 
        a::Int = 2
        b::Complex = 1 + 1im 
    end
end

vec_a = AT[rand((A,B))() for _ in 1:10^6];
vec_c = CT[rand((C,D))() for _ in 1:10^6];
vec_e = ET[rand((E,F))() for _ in 1:10^6];

We look both to time and memory:

julia> @btime sum(x.a for x in $vec_a);
  937.911 μs (0 allocations: 0 bytes)

julia> @btime sum(x.a for x in $vec_c);
  715.359 μs (0 allocations: 0 bytes)

julia> @btime sum(x.a for x in $vec_e);
  3.936 ms (0 allocations: 0 bytes)

julia> Base.summarysize(vec_a)
35791632

julia> Base.summarysize(vec_c)
35487008

julia> Base.summarysize(vec_e)
12820240

As you can see, in this very simple (and so not too informative) benchmark @compact_sum_type is both faster and nearly as memory efficient, while @sum_struct_type is much more memory efficient that the other two macros.

For those interested, the package is already available in the general registry. Issues and PRs are really welcomed :slight_smile:

10 Likes

MixedStructTypes.jl reached version 0.2 with an improved syntax e.g. in the example above:

@compact_structs CT begin
    @kwdef struct C 
        a::Int = 1 
    end
    @kwdef struct D 
        a::Int = 2
        b::Complex = 1 + 1im 
    end
end

I also fixed and tested many edge cases, and made some new enhancements like the ability to constrain type parameters. Let me know if you have suggestions on new functionalities!