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 DataType
s. 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
?)