Type-stable `(...)` tuples of types?

Hi all!

I have an ECS (entity component system) API and try to make type-stable use of tuples of types. Currently, I have it working well with VarArgs, for example:

pos, vel = get_components(world, entity, Position, Velocity)

However, I would prefer a tuple for the types, like this:

pos, vel = get_components(world, entity, (Position, Velocity))

Unfortunately, I am not able to get this working in a type-stable way.

My current working var-args approach looke like this:

unction get_components(world::World{CS,CT,N}, entity::Entity, comp_types::Type...) where {CS<:Tuple,CT<:Tuple,N}
    if !is_alive(world, entity)
        error("can't get components of a dead entity")
    end
    return _get_components(world, entity, Val{Tuple{comp_types...}}())
end

@generated function _get_components(world::World{CS,CT,N}, entity::Entity, ::Val{TS}) where {CS<:Tuple,CT<:Tuple,N,TS<:Tuple}
    types = TS.parameters
    if length(types) == 0
        return :(())
    end

    exprs = Expr[]
    push!(exprs, :(idx = world._entities[entity._id]))

    for i in 1:length(types)
        T = types[i]
        stor_sym = Symbol("stor", i)
        col_sym = Symbol("col", i)
        val_sym = Symbol("v", i)

        push!(exprs, :(
            $(stor_sym) = _get_storage(world, Val{$(QuoteNode(T))}())
        ))
        push!(exprs, :(
            $(col_sym) = $(stor_sym).data[idx.archetype]
        ))
        push!(exprs, :(
            $(val_sym) = $(col_sym)._data[idx.row]
        ))
    end

    vals = [:($(Symbol("v", i))) for i in 1:length(types)]
    push!(exprs, Expr(:return, Expr(:tuple, vals...)))

    return quote
        @inbounds begin
            $(Expr(:block, exprs...))
        end
    end
end

I found two ways to use tuples in the API, but both are extemely slow due to type instability:

function get_components(world::World{CS,CT,N}, entity::Entity, comp_types::Tuple{Vararg{Type}}) where {CS<:Tuple,CT<:Tuple,N}
    if !is_alive(world, entity)
        error("can't get components of a dead entity")
    end
    return _get_components(world, entity, Val{Tuple{comp_types...}}())
end

@generated function _get_components(world::World{CS,CT,N}, entity::Entity, ::Val{TS}) where {CS<:Tuple,CT<:Tuple,N,TS<:Tuple}
function get_components(world::World{CS,CT,N}, entity::Entity, comp_types::Tuple{Vararg{Type}}) where {CS<:Tuple,CT<:Tuple,N}
    if !is_alive(world, entity)
        error("can't get components of a dead entity")
    end
    return _get_components(world, entity, Tuple{comp_types...})
end

@generated function _get_components(world::World{CS,CT,N}, entity::Entity, ::Type{TS}) where {CS<:Tuple,CT<:Tuple,N,TS<:Tuple}

So is there any way to properly convert my (Position, Velocity) to Tuple{Position, Velocity} (I guess) in a type-stable way?

Thank you in advance!

A possible way to side-step this is using Val in your tuple. I suspect this is the only way to make the generated function approach work well because a generated function has access to the types so without the Val, everything is just DataType. So my best guess is that get_components(world, entity, (Val(Position), Val(Velocity))) would work fine, and you could make this macro if you want @get_components(world, entity, (Position, Velocity)) for convenience.

1 Like

There was a trick here with @generated.: Puzzling inference with `Base.Fix` · Issue #59928 · JuliaLang/julia · GitHub

And the Base.Fix uses a Base._stable_typeof to handle some of the problems with types as arguments. I.e. typeof(Int) returns DataType, whereas _stable_typeof(Int) returns the sharper Type{Int}.

One of the basic problems is that you can’t make Tuple{Type{Int}}((Int,)) without losing the type information. It can be done with Val, or with NamedTuple with an explicit constructor like NamedTuple{(:a, :b), Tuple{Type{Int8}, Type{Int16}}}((Int8, Int16)). It can then be accessed with fieldtypes instead of .parameters.

2 Likes

Thank you! We settled for the Val variant in the API.