DataTypes as function arguments

Let’s say I want to define a function tmap which applies a given function to several arguments, where – this is the tricky part – the arguments are DataTypes. My first try was the “obvious”

tmap1(f, xs...) = map(f, xs)

which works

julia> tmap1(zero, Int, Float64, UInt8)
(0, 0.0, 0x00)

but the return-type can not be inferred:

julia> @code_warntype tmap1(zero, Int, Float64, UInt8)
Variables:
  #self#::#tmap1
  f::Base.#zero
  xs::Tuple{DataType,DataType,DataType}

Body:
  begin 
      return (Core.tuple)((f::Base.#zero)((Base.getfield)(xs::Tuple{DataType,DataType,DataType}, 1)::DataType)::Any, (f::Base.#zero)((Base.getfield)(xs::Tuple{DataType,DataType,DataType}, 2)::DataType)::Any, (f::Base.#zero)((Base.getfield)(xs::Tuple{DataType,DataType,DataType}, 3)::DataType)::Any)::Tuple{Any,Any,Any}                                                                                                                                             
  end::Tuple{Any,Any,Any}

So I tried the “recursion trick”, as follows:

tmap2(f, x, xs...) = (f(x), tmap2(f,xs...)...)
tmap2(f) = ()

which works only slight better:

julia> @code_warntype tmap2(zero, Int, Float64, UInt8)
Variables:
  #self#::#tmap2
  f::Base.#zero
  x::Any
  xs::Tuple{DataType,DataType}

Body:
  begin 
      return (Core.tuple)(0, ($(QuoteNode(zero)))((Core.getfield)(xs::Tuple{DataType,DataType}, 1)::DataType)::Any, ($(QuoteNode(zero)))((Core.getfield)(xs::Tuple{DataType,DataType}, 2)::DataType)::Any)::Tuple{Int64,Any,Any}
  end::Tuple{Int64,Any,Any}

i.e. return-type inference is still incomplete. In my understanding the inference fails because typeof(Int)==DataType instead of typeof(Int)==Type{Int} so that the xs in tmap1 and tmap2 becomes a Tuple{DataType,...}, losing the type information. (Correct?)

Now I have come up with a solution using generated functions, as follows (I can’t seem to get the quoting right, so I turned to Expr):

tget{T}(t::Type{Type{T}}) = T   # helper function
@generated function tmap3(f, xs...)
    args = Any[Expr(:call, :f, tget(x)) for x in xs]
    Expr(:tuple, args...)
end

for which return-type inference succeeds:

@code_warntype tmap3(zero, Int, Float64, UInt8)
Variables:
  #self#::#tmap3
  f::Base.#zero
  xs::Any

Body:
  begin  # line 1:
      return (Core.tuple)(0, (Base.sitofp)(Float64, 0)::Float64, (Base.checked_trunc_uint)(UInt8, 0)::UInt8)::Tuple{Int64,Float64,UInt8}
  end::Tuple{Int64,Float64,UInt8}

(I believe the xs::Any is harmless.)

This works on 0.5 and 0.6, but I wonder if this is a correct and future-proof approach. As far as I can figure out, it happens to work because during the generation stage of the generated function (in which the arguments are replaced by their types), the argument types are actually Type{Int} etc. despite typeof(Int)==DataType. Proof:

julia> @generated function test(xs...); dump(xs); nothing; end
test (generic function with 1 method)

julia> test(Int, Float64, UInt8)
Tuple{DataType,DataType,DataType}
  1: Type{Int64} <: Any
  2: Type{Float64} <: Any
  3: Type{UInt8} <: Any

My question is: Is this behaviour something I can rely on, or is this an (accidental?) implementation detail? (Or alternatively, is there a simpler type-stable way of writing tmap?)

The behavior of generated functions there is https://github.com/JuliaLang/julia/issues/12783 and should not be relied upon.