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?)