[ANN] TypeParams.jl - Generic type parameters without the fuss

Problem

A key feature of Julia is that type annotations are not usually needed to achieve optimal performance.
For example, foo(1,2) runs equally fast regardless whether foo() is defined as foo(a,b) = ... or foo(a::Int, b::Float64) = .... Unfortunately, there is an important exception to this rule: writing

struct Foo
    a
    b
end

instead of

struct Foo
    a::Int
    b::Float64
end

will discard all compile-time type information on a and b and hence incur a significant performance penalty. A common workaround to this problem is to introduce a new type parameter for each field:

struct Foo{A,B}
    a::A
    b::B
end

This recovers the flexibility of optional typing and preserves the performance of compile-time types, but keeping the fields and type parameters in sync can be laborious.

Solution

TypeParams eliminates the fuss of generic type parameters by introducing a macro @typeparams which allows you to insert such type parameters using a simple syntax:

@typedef struct Foo
    a::{}
    b::{}
end

It further supports expressing type constraints with zero syntax overhead:

@typedef struct Foo
    a::{<:Integer}
    b::{<:Real}
end

Finally, @typeparams plays well with other features of the Julia language:

  • Explicit type parameters:

    @typeparams struct MyVector{T} <: AbstractVector{T}
        data::{<:AbstractVector{T}}
    end
    Base.size(v::MyVector) = size(v.data)
    Base.getindex(v::MyVector, i::Int) = v.data[i]
    
    julia> MyVector([1,2,3])
    3-element MyVector{Int64, Vector{Int64}}:
    ...
    
  • The @kwdef macro:

    Base.@kwdef @typeparams struct Foo
        a::{} = 1
        b::{} = 1.0
    end
    
    julia> Foo()
    Foo{Int64, Float64}(1, 1.0)
    

Acknowledgements

This package is heavily inspired by AutoParameters.jl.

23 Likes

Is TypeParams compatible with Parameters.@with_kw (goes a bit further than Base.@kwdef) as well?

This package is very similar to ConcreteStructs.jl, which I often use.

Recently, I tried to make ConcreteStructs.jl usable in combination with Parameters.jl. My personal conclusion is that it is necessary to change Parameters.@with_kw expanding the macros in the argument expression, like Base.@kwdef.

See

Then TypeParams.jl also becomes compatible with Parameters.jl.

Example:

using TypeParams
using Parameters

@eval Parameters macro with_kw(typedef)
    typedef = macroexpand(__module__, typedef) # inserted
    return esc(with_kw(typedef, __module__, true))
end

@with_kw @typeparams struct Foo_with_kw
    a::{} = 1
    b::{} = 2.0
    c::{<:AbstractString} = "three"
end

Foo_with_kw()

Output:

Foo_with_kw{Int64, Float64, String}
  a: Int64 1
  b: Float64 2.0
  c: String "three"

Jupyter notebook: https://github.com/genkuroki/public/blob/main/0019/On%20TypeParams.jl.ipynb

2 Likes

No, @typeparams is currently not compatible with @with_kw, as @genkuroki has already pointed out. @genkuroki’s fix of making @with_kw expand its argument first works in simple cases, but I expect it would break the @assert and @deftype features of @with_kw.

Maybe @with_kw @typeparams could be made to work by deleting the macroexpand in @typeparams, but then @typeparams may also require quite a bit of extra work to make it handle the potentially vastly more complicated input.

Personally, I would love to see such improvement in native Julia without explicit usage of macros from packages. It seems quite natural addition for performance and is rare to see a situation where Any type is preferred.

14 Likes

It would indeed really be nice if we could just write

struct MyStruct{T<:Integer}
    a::T = 1
    b<:Real = 2.0
    c<:AbstractArray{<:Real} = [3, 4, 5]
end

resulting in additional “anonymous” type parameters. There is, of course, some abuse potential - instead of using the same type parameter for several fields (where appropriate), it would be easier for users to use the syntax above, resulting in types with (potentially) a large number of type parameters.