Use function on type parameter to decide struct field type at compile time

This code does not work, but I want to do something like the following (the general idea should be clear):

map_type(::Type{String}) = Int64
map_type(::Type{Int64}) = String

struct Foo{T}
    a::map_type(T)
end

Basically the function map_type can be fully resolved at compile time so one could imagine a world where this works. But if you enter this in to the REPL you will see

ERROR: MethodError: no method matching map_type(::TypeVar)
Closest candidates are:
  map_type(::Type{String})
  map_type(::Type{Int64})

Is there a way to achieve such a thing? Presumably in a real world use case the map_type function would be a little more complicated. A motivating example may be to do the following transformation:

map_type(::Val{N}, ::Type{T}) where T <: NTuple{N, Real} where N = begin
    [Foo{fieldtype(T, i)} for i in 1:N]
end

struct Foo{T <: Real}
    a :: T
end

struct Bar{N, T <: NTuple{N, Real}}
    foos::Tuple{map_type(Val(N), T)...}
end

This doesn’t work, but the idea is to allow Bar to contain a variable number of differently parameterized Foos in a type-stable way.

I understand this is bordering abuse of the type system, but I have a use case in mind for creating a time series iterator that merges a variable number of other time series iterators (output in chronological order from multiple iterators). Obviously the output of such an iterator would not be type-stable (unless all underlying iterators output the same type), but it could be restricted to a small Union type so that the compiler could still use a union splitting optimization for performance. (Or at least that is my theory)

There’s no way to do this the way you’ve specified it, unfortunately. Generally you’d work around this by adding another type parameter, like:

struct Foo{T, V}
  a::V
  
  Foo{T}(a) where {T} = new{T, map_type(T)}(a)
end

where the inner constructor is responsible for computing V. The ComputedFieldTypes package has a couple of macros to make that pattern a little bit easier.

1 Like

So just to see if I understand, I could do the more complicated goal I was trying to achieve simply with:

struct Foo{T <: Real}
    a :: T
end

struct Bar{T}
    foos::T

    Bar(foos) = begin
        new{Tuple{[typeof(f) for f in foos]...}}(foos)
    end
end

f1 = Foo(1)
f2 = Foo(2.0)
b = Bar((f1, f2))
typeof(b) === Bar{Tuple{Foo{Int64}, Foo{Float64}}} # true

Moreover will any function I call with b that access the underlying foos generate efficient code since the compiler knows the types of each foo?

For example in:

test_func(b::Bar) = begin
    s = 0.0    
    for f in b.foos
        s += f.a
    end
end

will the compiler unfold the loop and insert the appropriate addition calls since it knows the type of each f exactly?

Actually based on the output of @code_warntype using a for loop does still yield type instability because the type of f is different on each iteration.

Replacing the for loop with individual indexing:

s += b.foos[1].a
s += b.foos[2].a

yields completely type stable code. Look at this interesting thread for interesting general approaches that don’t involve hardcoding Type stability while looping over Tuple. Namely recursion and the Unrolled.jl package