Type-stable zip to pairs

I have an interesting puzzle that I haven’t been able solve (without resorting to @generated functions).

I’d like to write a function pairzip(x::Tuple, y::Tuple) such that

pairzip((a, b), (c, d)) == (a=>c, b=>d)

in a type-stable way, even when the types of a, b, c, and d may all be different. This is easy to do with a generated function:

@generated function pairzip(x::Tuple{Vararg{Any, N}}, y::Tuple{Vararg{Any, N}}) where {N}
    Expr(:tuple, [:(x[$i] => y[$i]) for i in 1:N]...)
end

but I’m curious if there’s another way. Surely there must be some recursive trick I’m missing :wink:

Any ideas?

1 Like
julia> pairzip(t::Tuple{}, u::Tuple{}) = ()
pairzip (generic function with 1 method)

julia> pairzip(t::Tuple{}, u::Tuple) = throw(ArgumentError("args must be equal length"))
pairzip (generic function with 2 methods)

julia> pairzip(t::Tuple, u::Tuple) = (t[1] => u[1], pairzip(Base.tail(t), Base.tail(u))...)
pairzip (generic function with 3 methods)

julia> pairzip((1, 2), (3, 4))
(1=>3, 2=>4)

julia> @code_warntype pairzip((1, 2), (3, 4))
Variables:
  #self#::#pairzip
  t::Tuple{Int64,Int64}
  u::Tuple{Int64,Int64}

Body:
  begin 
      return (Core.tuple)($(Expr(:new, Pair{Int64,Int64}, :((Base.getfield)(t, 1)::Int64), :((Base.getfield)(u, 1)::Int64))), $(Expr(:new, Pair{Int64,Int64}, :((Core.getfield)(t, 2)::Int64), :((Core.getfield)(u, 2)::Int64))))::Tuple{Pair{Int64,Int64},Pair{Int64,Int64}}
  end::Tuple{Pair{Int64,Int64},Pair{Int64,Int64}}

julia> @code_warntype pairzip((1, :x, r"x"), ("w", 1.0, +))
Variables:
  #self#::#pairzip
  t::Tuple{Int64,Symbol,Regex}
  u::Tuple{String,Float64,Base.#+}

Body:
  begin 
      SSAValue(1) = (Core.getfield)(t::Tuple{Int64,Symbol,Regex}, 2)::Symbol
      SSAValue(2) = (Core.getfield)(t::Tuple{Int64,Symbol,Regex}, 3)::Regex
      return (Core.tuple)($(Expr(:new, Pair{Int64,String}, :((Base.getfield)(t, 1)::Int64), :((Base.getfield)(u, 1)::String))), $(Expr(:new, Pair{Symbol,Float64}, SSAValue(1), :((Core.getfield)(u, 2)::Float64))), $(Expr(:new, Pair{Regex,Base.#+}, SSAValue(2), :($(QuoteNode(+))))))::Tuple{Pair{Int64,String},Pair{Symbol,Float64},Pair{Regex,Base.#+}}
  end::Tuple{Pair{Int64,String},Pair{Symbol,Float64},Pair{Regex,Base.#+}}

Here’s my solution, using 3 methods.

5 Likes

Yes!! This is perfect, thank you!!

You can also use the ntuple function to push the complexity of getting the right algorithm into a lower layer (fengyang.wang’s solution is O(N^2), while ntuple is O(N), like your original code)

function pairzip(x::Tuple{Vararg{Any, N}}, y::Tuple{Vararg{Any, N}}) where {N}
    return ntuple(i -> (x[i] => y[i]), Val{N})
end
1 Like

@jameson that was the first thing I tried, but it seems not to be type-stable for heterogeneous tuples (presumably since the anonymous function’s output isn’t a consistent type?)

julia> function pairzip(x::Tuple{Vararg{Any, N}}, y::Tuple{Vararg{Any, N}}) where {N}
           return ntuple(i -> (x[i] => y[i]), Val{N})
       end
pairzip (generic function with 1 method)

julia> pairzip((1, 1.0), ("x", :y))
(1=>"x", 1.0=>:y)

julia> @code_warntype pairzip((1, 1.0), ("x", :y))
Variables:
  #self#::#pairzip
  x::Tuple{Int64,Float64}
  y::Tuple{String,Symbol}
  #1::##1#2{Tuple{Int64,Float64},Tuple{String,Symbol}}

Body:
  begin 
      #1::##1#2{Tuple{Int64,Float64},Tuple{String,Symbol}} = $(Expr(:new, ##1#2{Tuple{Int64,Float64},Tuple{String,Symbol}}, :(x), :(y)))
      $(Expr(:inbounds, false))
      # meta: location tuple.jl ntuple 128
      # meta: location tuple.jl _ntuple 135
      SSAValue(4) = $(Expr(:invoke, MethodInstance for (::##1#2{Tuple{Int64,Float64},Tuple{String,Symbol}})(::Int64), :(#1), :((Base.add_int)(0, 1)::Int64)))
      # meta: location tuple.jl _ntuple 135
      SSAValue(3) = (Core.tuple)(SSAValue(4), $(Expr(:invoke, MethodInstance for (::##1#2{Tuple{Int64,Float64},Tuple{String,Symbol}})(::Int64), :(#1), :((Base.add_int)(1, 1)::Int64))))::Tuple{Any,Any}
      # meta: pop location
      # meta: pop location
      # meta: pop location
      $(Expr(:inbounds, :pop))
      return SSAValue(3)
  end::Tuple{Any,Any}