Poor typing of tuple generators

I’m used to writing tuple comprehensions using tuple((blah(x) for x in tup)...), but I just found out that it’s not type-stable:

add2(tup) = tuple((x+2 for x in tup)...)
@code_warntype add2((1,2.0,3))      # ::Tuple{Vararg{Union{Float64, Int64},N} where N}

In contrast, add2(tup) = tuple(map(x->x+2, tup)...) is fine (the tuple() is redundant, I know). Are tuple comprehensions just a bad idea in general, or am I doing it wrong?

This isn’t a direct answer to your question, but there’s a better option in Julia v0.6:

julia> add2(x) = x + 2
add2 (generic function with 1 method)

julia> t = (1, 2.0, 3)
(1, 2.0, 3)

julia> add2.(t)
(3, 4.0, 5)

julia> @code_warntype add2.(t)
Variables:
  #self#::Base.#broadcast
  f::#add2
  t::Tuple{Int64,Float64,Int64}
  ts::Tuple{}

Body:
  begin
      return (Core.tuple)((Base.add_int)((Base.getfield)(t::Tuple{Int64,Float64,Int64}, 1)::Int64, 2)::Int64, (Base.add_float)((Base.getfield)(t::Tuple{Int64,Float64,Int64}, 2)::Float64, (Base.sitofp)(Float64, 2)::Float64)::Float64, (Base.add_int)((Base.getfield)(t::Tuple{Int64,Float64,Int64}, 3)::Int64, 2)::Int64)::Tuple{Int64,Float64,Int64}
  end::Tuple{Int64,Float64,Int64}
2 Likes

.-broadcast exists on v0.5 as well.

True, but it doesn’t handle tuples on v0.5:

julia> add2(x) = x + 2
add2 (generic function with 1 method)

julia> t = (1, 2.0, 3)
(1,2.0,3)

julia> add2.(t)
ERROR: MethodError: Cannot `convert` an object of type Tuple{Int64,Float64,Int64} to an object of type CartesianRange{I<:CartesianIndex}
This may have arisen from a call to the constructor CartesianRange{I<:CartesianIndex}(...),
since type constructors fall back to convert methods.
 in CartesianRange{I<:CartesianIndex}(::Tuple{Int64,Float64,Int64}) at ./sysimg.jl:53
 in broadcast_t(::Function, ::Type{Any}, ::Tuple{Int64,Float64,Int64}, ::Vararg{Tuple{Int64,Float64,Int64},N}) at ./broadcast.jl:214
 in broadcast(::Function, ::Tuple{Int64,Float64,Int64}) at ./broadcast.jl:230
1 Like

Note that it works on Julia 0.5 using Compat (see PR #324):

julia> using Compat

julia> add2(x) = x + 2
add2 (generic function with 1 method)

julia> add2.((1, 2.0, 3))
(3,4.0,5)
2 Likes

You can use tuple types as constructors, e.g. NTuple{4,Int}(2x for x in 1:4) or Tuple(2x for x in 1:4).

1 Like

The problem is that none of these generator solutions can be type-stable on a heterogeneous input tuple. I was hoping the compiler would automatically unroll (x+2 for x in tup)..., but I suppose the map/broadcast solutions are sufficient.

If you know the function within your generator preserves the type, you can use the type of the original tuple as the constructor:

julia> add2(tup) = typeof(tup)((x+2 for x in tup)...)
add2 (generic function with 1 method)

julia> @code_warntype add2((1,2.0,3))
Variables:
  #self#::#add2
  tup::Tuple{Int64,Float64,Int64}
  #5::##5#6

Body:
  begin
      SSAValue(0) = Tuple{Int64,Float64,Int64}
      SSAValue(2) = $(Expr(:new, Base.Generator{Tuple{Int64,Float64,Int64},##5#6}, :($(QuoteNode(#5))), :(tup)))
      return (Core._apply)(SSAValue(0), SSAValue(2))::Tuple{Int64,Float64,Int64}
  end::Tuple{Int64,Float64,Int64}
3 Likes