Hi all,
I’d like to announce the package WrappedUnions.jl. This package originates by my better understanding of type instabilities caused by using union of types and offers a better interface than my previous attempt LightSumTypes.jl.
I think that its interface is the most Julian across all packages which work with sumtypes, and it’s actually also very powerful, it can easily accomodates a lot of use cases where one wants to force the compiler to emit type-stable code. I think that it could also be very useful for trimming pipelines e.g the one described in this post: [Guide] Using sum types to define dynamic C APIs for Julia 1.12 `--trim`.
Two main macros provide the backbone of this package: @wrapped
and @unionsplit
. The first accepts any parametric struct which has a single fields union::Union
and whose abstract type is a subtype of WrappedUnion
, but, apart from that, it supports any standard struct feature as e.g. inner constructors. @unionsplit
instead automatically executes a function performing union-splitting on the wrapped union arguments to make the call type-stable.
I give you a little example of its main features. Here we split automatically the sum
function so that the code is inferred correctly
julia> using WrappedUnions, Test, BenchmarkTools
julia> xs_nowrap = (false, 1, [true, false], [1,2]) # what happens if no wrapping is used?
(false, 1, Bool[1, 0], [1, 2])
julia> @inferred (xs -> sum.(xs))(xs_nowrap);
ERROR: return type NTuple{4, Int64} does not match inferred return type NTuple{4, Any}
Stacktrace:
[1] error(s::String)
@ Base ./error.jl:35
[2] top-level scope
@ REPL[3]:1
julia> @btime sum.($xs_nowrap)
136.393 ns (5 allocations: 240 bytes)
(0, 1, 1, 3)
julia> @wrapped struct X
union::Union{Bool, Int, Vector{Bool}, Vector{Int}}
end
julia> xs = (X(false), X(1), X([true, false]), X([1,2]))
(X(false), X(1), X(Bool[1, 0]), X([1, 2]))
julia> splitsum(x) = @unionsplit sum(x)
splitsum (generic function with 1 method)
julia> @inferred (xs -> splitsum.(xs))(xs); # no error!
julia> @btime splitsum.($xs)
8.494 ns (0 allocations: 0 bytes)
(0, 1, 1, 3)
Let’s do something more fancy: what if the output is type unstable? Another wrapping comes to our rescue:
julia> @btime prod.($xs_nowrap)
164.485 ns (5 allocations: 240 bytes)
(false, 1, false, 2)
julia> splitprod(x::X) = @unionsplit prod(x)
f (generic function with 2 methods)
julia> @inferred (xs -> splitprod.(xs))(xs) # not enough
ERROR: return type Tuple{Bool, Int64, Bool, Int64} does not match inferred return type NTuple{4, Union{Bool, Int64}}
Stacktrace:
[1] error(s::String)
@ Base ./error.jl:35
[2] top-level scope
@ REPL[20]:1
julia> @btime splitprod.($xs)
97.827 ns (1 allocation: 48 bytes)
(false, 1, false, 2)
julia> @wrapped struct Y <: WrappedUnion
union::Union{Bool, Int}
end
julia> splitprod(x::X) = Y(@unionsplit prod(x));
julia> @inferred (xs -> splitprod.(xs))(xs); # But now it is!
julia> @btime splitprod.($xs)
14.536 ns (0 allocations: 0 bytes)
(Y(false), Y(1), Y(false), Y(2))
For more information, see the ReadMe of the package.
The package is still in registration so use ]dev https://github.com/Tortar/WrappedUnions.jl.git
if you want to try it out.
Let me know if you have any kind of question/suggestion about this approach!