# 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

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}
``````