Dispatch on singleton type inside a tuple

question
inference

#1

Hello,

in order to dispatch on a datatype known at runtime, we can use singleton types:

func1(::Type{T},n) where T = Array{T}(n)

which seems to give a type-stable result:

julia> @code_warntype func1(Int32,10)
Variables:
  #self#::#func1
  #unused#::Any
  n::Int64

Body:
  begin 
      return $(Expr(:foreigncall, :(:jl_alloc_array_1d), Array{Int32,1}, svec(Any, Int64), Array{Int32,1}, 0, :(n), 0))
  end::Array{Int32,1}

Here the return type is inferred correctly. I believe the #unused#::Any is not a problem, though I’m not sure.

I wonder how I can achieve the same type-stable behavior if my func function doesn’t take the datatype and integer parameters separately, but combined in a tuple. (The eventually intended function should take a variable number of such tuples.) My first try:

func2(spec::Tuple{Type{T},Int}) where T = Array{T}(spec[2])

doesn’t work:

julia> func2((Int32,10))
ERROR: MethodError: no method matching func2(::Tuple{DataType,Int64})

seemingly because the type of (Int32,10) is Tuple{DataType,Int64}, not Tuple{Type{Int32},Int64}. But if I define my func accordingly,

func3(spec::Tuple{DataType,Int}) = Array{spec[1]}(spec[2])

then the return type can no longer be inferred:

julia> @code_warntype func3((Int32,10))
Variables:
  #self#::#func3
  spec::Tuple{DataType,Int64}

Body:
  begin 
      return ((Core.apply_type)(Main.Array, (Base.getfield)(spec::Tuple{DataType,Int64}, 1)::DataType)::Type{Array{_,N} where N} where _)((Base.getfield)(spec::Tuple{DataType,Int64}, 2)::Int64)::Array{_,1} where _
  end::Array{_,1} where _   # inferred return type is not concrete!

Adding a function barrier, i.e.

function func4(spec::Tuple{DataType,Int}); T,n=spec; func1(T,n); end

also doesn’t help.

Is it possible to properly dispatch on the singleton type inside the tuple? How? Thanks.


#2

See Trick 1 here:


#3

It’s related to this issue. I haven’t found a clean solution, but wrapping the type in Val “solves” the problem.

julia> func5{T}(spec::Tuple{Val{T}, Int}) = Vector{T}(spec[2])
func5 (generic function with 1 method)

julia> func5((Val{Int32}(), 10))   # type-stable

#5

Thanks, @Tamas_Papp. But I’m afraid this trick doesn’t work for the case of having types inside the tuple. If I understand correctly, what I should do according to the trick is:

# func1 corresponds to "argtail" in stevengj's notebook
func1(::Type{T}, dims...) where T = Array{T}(dims...)
# func2 corresponds to "tupletail"
func2(spec) = func1(spec...)

With this, the return-type inference works for func1:

@code_warntype func1(Int32,10)
Variables:
  #self#::#func1
  #unused#::Any
  dims::Tuple{Int64}

Body:
  begin 
      return $(Expr(:foreigncall, :(:jl_alloc_array_1d), Array{Int32,1}, svec(Any, Int64), Array{Int32,1}, 0, :((Core.getfield)(dims, 1)::Int64), 0))
  end::Array{Int32,1}

but not for func2:

Variables:
  #self#::#func2
  spec::Tuple{DataType,Int64}

Body:
  begin 
      return (Main.func1)((Core.getfield)(spec::Tuple{DataType,Int64}, 1)::DataType, (Core.getfield)(spec::Tuple{DataType,Int64}, 2)::Int64)::Any
  end::Any   # not even Array{_,1} !

It’s even worse than my original approach, as it doesn’t infer that the return type is an Array.

If I misunderstood how to apply the trick to my case, please help me get it right.

I believe the problem comes from typeof(Int32)==DataType instead of typeof(Int32)==Type{Int32}.


#6

Thanks, @cstjean. The issue #10947 is spot-on. If anyone is collecting votes for making typeof(T)==Type{T} (where T isa DataType), you’d have my vote. :slight_smile:

The workaround of using Val{T} instead of Type{T} indeed works beautifully. Unfortunately for my intended use case, the goal is to have a convenient (i.e. as short as possible) way for the user to specify a datatype and one (or several) integers, so the Val approach falls short. I had hoped to achieve this without macros, but maybe it’s not possible.


#7

You could also create a type so that the user passes Spec{Int}(12).


#8

The custom type approach that @cstjean suggested is also more easily extended in the future if you decide to add, for example, a second optional argument to the Spec constructor.


#9

Thanks, @cstjean and @rdeits, for the suggestion of using a dedicated type. This surely works, but it doesn’t help much, as the user already has the option to just pass Array{T}(n). I just wanted a way for the user to pass (T,n) instead, as there are potentially many such specifications to pass. (For the record, the original problem occurs in my package FortranFiles.jl, and the code I’m trying to optimize is in the read_spec function.)

There is actually another similar problem, which not directly involves tuples. It boils down to this:

julia> zz(xs...) = map(zero, xs)

If called with regular types, the return-type inference works well:

Variables:
  #self#::#zz
  xs::Tuple{Int64,Float64,UInt8}

Body:
  begin 
      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}

but if I replace one argument with a datatype, inference fails:

@code_warntype zz(Int, 1.0, 0x01)
Variables:
  #self#::#zz
  xs::Tuple{DataType,Float64,UInt8}

Body:
  begin 
      return (Core.tuple)((Main.zero)((Base.getfield)(xs::Tuple{DataType,Float64,UInt8}, 1)::DataType)::Any, (Base.sitofp)(Float64, 0)::Float64, (Base.checked_trunc_uint)(UInt8, 0)::UInt8)::Tuple{Any,Float64,UInt8}
  end::Tuple{Any,Float64,UInt8}   # Any instead of Int64

I tried to apply @stevengj’s recursive argument processing trick which @Tamas_Papp suggested, as follows:

julia> zz() = ()
julia> zz(x, xs...) = (zero(x), zz(xs...)...)

The return-type is now inferred correctly if the first argument is a datatype:

julia> @code_warntype zz(Int, 1.0, 0x01)
Variables:
  #self#::#zz
  x::Any
  xs::Tuple{Float64,UInt8}

Body:
  begin 
      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}   # Yay!

(though note the x::Any warning), but not if a later argument is a datatype:

julia> @code_warntype zz(1, Float64, 0x01)
Variables:
  #self#::#zz
  x::Int64
  xs::Tuple{DataType,UInt8}

Body:
  begin 
      return (Core.tuple)(0, (Main.zero)((Core.getfield)(xs::Tuple{DataType,UInt8}, 1)::DataType)::Any, (Base.checked_trunc_uint)(UInt8, 0)::UInt8)::Tuple{Int64,Any,UInt8}                                                                                                                                       
  end::Tuple{Int64,Any,UInt8}   # again Any :(

Any advice on how to fix this is appreciated!


#10

Technically, you could write a macro that creates the definitions for zz(a) = ..., zz(a, b) = ..., etc. But that’s not very clean. Could you write instead a macro like this?

my_spec = @spec begin
x => (Int, Float64, 5)
y => (Float64,)
...
end

Then it’s easy to make type-stable.