Type instability with (fairly) simple Tuples

Should I expect the following to be type-stable? I think the types are inferrable in principle, but maybe the Tuple-of-Pairs structure is just too ‘complicated’?

julia> using Cthulhu

julia> function foo(t::Pair{Symbol,Int64}...)
       t1 = Tuple(x[1] for x in t)
       t2 = Tuple(x[2] for x in t)
       return nothing
       end
foo (generic function with 1 method)

julia> @descend foo(:a=>1, :b=>2, :c=>3)
foo(t::Pair{Symbol, Int64}...) @ Main REPL[25]:1
1 function foo(t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}}::Pair{Symbol,Int64}...)::Core.Const(nothing)
2 t1::Tuple{Vararg{Symbol}} = Tuple(x[1] for x in t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}}::Base.Generator{Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}}, var"#foo##12#foo##13"})
3 t2::Tuple{Vararg{Int64}} = Tuple(x[2] for x in t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}}::Base.Generator{Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}}, var"#foo##14#foo##15"})
4 return nothing::Core.Const(nothing)
5 end
Select a call to descend into or ↩ to ascend. [q]uit. [b]ookmark.
Toggles: [w]arn, [h]ide type-stable statements, [t]ype annotations, [s]yntax highlight for Source/LLVM/Native, [j]ump to source always.
Show: [S]ource code, [A]ST, [T]yped code, [L]LVM IR, [N]ative code
Actions: [E]dit source code, [R]evise and redisplay
 •  x in t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}}
   t1
    x in t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}}
   t2
   ↩

I tried several variations, e.g. making t an NTuple rather than splatting, but didn’t find any that made any difference.

This is an issue for me because for example trying to use t2 to create an array a = zeros(t2...) will not know the dimension of the array, introducing more type instability. Going to try to work around this somehow, but suspect alternatives will make my code a little bit less readable…

I think the problem is that using comprehension syntax lowers to Base.Generator which doesn’t have the length in the type. So you’d need the length to be constant-propagated by the compiler, which I guess it isn’t.

Anyway the answer here is to use map(x->x[1], t) or ntuple(i->t[i][1], length(t)). These should be type stable.

1 Like

Thanks @cjdoris.

Using map works!

julia> function foo_map(t::Pair{Symbol,Int64}...)
       t1 = map(x->x[1], t)
       t2 = map(x->x[2], t)
       return nothing
       end
foo_map (generic function with 1 method)

julia> @descend foo_map(:a=>1, :b=>2, :c=>3)
foo_map(t::Pair{Symbol, Int64}...) @ Main REPL[35]:1
1 function foo_map(t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}}::Pair{Symbol,Int64}...)::Core.Const(nothing)
2 t1::Tuple{Symbol, Symbol, Symbol} = map(x->x[1], t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})::Tuple{Symbol, Symbol, Symbol}
3 t2::Tuple{Int64, Int64, Int64} = map(x->x[2], t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})::Tuple{Int64, Int64, Int64}
4 return nothing::Core.Const(nothing)
5 end
Select a call to descend into or ↩ to ascend. [q]uit. [b]ookmark.
Toggles: [w]arn, [h]ide type-stable statements, [t]ype annotations, [s]yntax highlight for Source/LLVM/Native, [j]ump to source always.
Show: [S]ource code, [A]ST, [T]yped code, [L]LVM IR, [N]ative code
Actions: [E]dit source code, [R]evise and redisplay
 • map(x->x[1], t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})
   map(x->x[2], t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})
   ↩

However, using NTuple seems not to help.

julia> function foo_NTuple(t::Pair{Symbol,Int64}...)
       t1 = NTuple(i->t[i][1], length(t))
       t2 = NTuple(i->t[i][2], length(t))
       return nothing
       end
foo_NTuple (generic function with 1 method)

julia> @descend foo_NTuple(:a=>1, :b=>2, :c=>3)
foo_NTuple(t::Pair{Symbol, Int64}...) @ Main REPL[37]:1
1 function foo_NTuple(t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}}::Pair{Symbol,Int64}...)::Union{}
2 t1::Union{} = NTuple::Type{NTuple{N, T} where {N, T}}(i->t[i][1], length(t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})::Core.Const(3))::Union{}
3 t2 = NTuple(i->t[i][2], length(t))
4 return nothing
5 end
Select a call to descend into or ↩ to ascend. [q]uit. [b]ookmark.
Toggles: [w]arn, [h]ide type-stable statements, [t]ype annotations, [s]yntax highlight for Source/LLVM/Native, [j]ump to source always.
Show: [S]ource code, [A]ST, [T]yped code, [L]LVM IR, [N]ative code
Actions: [E]dit source code, [R]evise and redisplay
 • length(t)
   length(t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})
   ↩

and even if I make the length a type parameter, and use first() and second() instead of indexing

julia> function foo_NTuple2(t::Vararg{Pair{Symbol,Int64},N}) where N
       t1 = NTuple(i->first(t[i]), Val(N))
       t2 = NTuple(i->second(t[i]), Val(N))
       return nothing
       end
foo_NTuple2 (generic function with 1 method)

julia> @descend foo_NTuple2(:a=>1, :b=>2, :c=>3)
foo_NTuple2(t::Vararg{Pair{Symbol, Int64}, N}) where N @ Main REPL[43]:1
1 function (foo_NTuple2(t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}}::Vararg{Pair{Symbol,Int64},N}) where N)::Union{}
2 t1::Union{} = NTuple::Type{NTuple{N, T} where {N, T}}(i->first(t[i]), Val(N::Core.Const(3))::Core.Const(Val{3}()))::Union{}
3 t2 = NTuple(i->second(t[i]), Val(N))
4 return nothing
5 end
Select a call to descend into or ↩ to ascend. [q]uit. [b]ookmark.
Toggles: [w]arn, [h]ide type-stable statements, [t]ype annotations, [s]yntax highlight for Source/LLVM/Native, [j]ump to source always.
Show: [S]ource code, [A]ST, [T]yped code, [L]LVM IR, [N]ative code
Actions: [E]dit source code, [R]evise and redisplay
 • %3 = _typeof_captured_variable(::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})::Core.Const(Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})
   Val(N::Core.Const(3))
   ↩

You can try ntuple:

function foo_ntuple(t::Vararg{Pair{Symbol,Int64},N}) where N
    t1 = ntuple(i->t[i][1], Val(N))
    t2 = ntuple(i->t[i][2], Val(N))
    return nothing
end
1 Like

Oh yes ntuple not NTuple!

Interesting - ntuple does better, but seems to fail to get the type of the second entry (even though map was able to!) Actually using second was the problem here, ntuple is fine - see next post.

julia> function foo_ntuple(t::Vararg{Pair{Symbol,Int64},N}) where N
       t1 = ntuple(i->first(t[i]), Val(N))
       t2 = ntuple(i->second(t[i]), Val(N))
       return nothing
       end
foo_ntuple (generic function with 1 method)

julia> @descend foo_ntuple(:a=>1, :b=>2, :c=>3)
foo_ntuple(t::Vararg{Pair{Symbol, Int64}, N}) where N @ Main REPL[45]:1
1 function (foo_ntuple(t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}}::Vararg{Pair{Symbol,Int64},N}) where N)::Core.Const(nothing)
2 t1::Tuple{Symbol, Symbol, Symbol} = ntuple(i->first(t[i]), Val(N::Core.Const(3))::Core.Const(Val{3}()))::Tuple{Symbol, Symbol, Symbol}
3 t2::Tuple{Any, Any, Any} = ntuple(i->second(t[i]), Val(N::Core.Const(3))::Core.Const(Val{3}()))::Tuple{Any, Any, Any}
4 return nothing::Core.Const(nothing)
5 end
Select a call to descend into or ↩ to ascend. [q]uit. [b]ookmark.
Toggles: [w]arn, [h]ide type-stable statements, [t]ype annotations, [s]yntax highlight for Source/LLVM/Native, [j]ump to source always.
Show: [S]ource code, [A]ST, [T]yped code, [L]LVM IR, [N]ative code
Actions: [E]dit source code, [R]evise and redisplay
 • %3 = _typeof_captured_variable(::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})::Core.Const(Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})
   Val(N::Core.Const(3))
   ntuple(i->first(t[i]), Val(N::Core.Const(3))::Core.Const(Val{3}()))
   %13 = _typeof_captured_variable(::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})::Core.Const(Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})
   Val(N::Core.Const(3))
   ntuple(i->second(t[i]), Val(N::Core.Const(3))::Core.Const(Val{3}()))
   ↩

I think map may be my new best friend for working with Tuples :slight_smile:

Looks like second() was the problem, going back to indexing the Pairs works

julia> function foo_ntuple2(t::Vararg{Pair{Symbol,Int64},N}) where N
       t1 = ntuple(i->t[i][1], Val(N))
       t2 = ntuple(i->t[i][2], Val(N))
       return nothing
       end
foo_ntuple2 (generic function with 1 method)

julia> @descend foo_ntuple2(:a=>1, :b=>2, :c=>3)
foo_ntuple2(t::Vararg{Pair{Symbol, Int64}, N}) where N @ Main REPL[53]:1
1 function (foo_ntuple2(t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}}::Vararg{Pair{Symbol,Int64},N}) where N)::Core.Const(nothing)
2 t1::Tuple{Symbol, Symbol, Symbol} = ntuple(i->t[i][1], Val(N::Core.Const(3))::Core.Const(Val{3}()))::Tuple{Symbol, Symbol, Symbol}
3 t2::Tuple{Int64, Int64, Int64} = ntuple(i->t[i][2], Val(N::Core.Const(3))::Core.Const(Val{3}()))::Tuple{Int64, Int64, Int64}
4 return nothing::Core.Const(nothing)
5 end
Select a call to descend into or ↩ to ascend. [q]uit. [b]ookmark.
Toggles: [w]arn, [h]ide type-stable statements, [t]ype annotations, [s]yntax highlight for Source/LLVM/Native, [j]ump to source always.
Show: [S]ource code, [A]ST, [T]yped code, [L]LVM IR, [N]ative code
Actions: [E]dit source code, [R]evise and redisplay
 • %3 = _typeof_captured_variable(::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})::Core.Const(Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})
   Val(N::Core.Const(3))
   ntuple(i->t[i][1], Val(N::Core.Const(3))::Core.Const(Val{3}()))
   %13 = _typeof_captured_variable(::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})::Core.Const(Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})
   Val(N::Core.Const(3))
   ntuple(i->t[i][2], Val(N::Core.Const(3))::Core.Const(Val{3}()))
   ↩

Also the Val(N) bit was unnecessary

julia> function foo_ntuple3(t::Pair{Symbol,Int64}...)
       t1 = ntuple(i->t[i][1], length(t))
       t2 = ntuple(i->t[i][2], length(t))
       return nothing
       end
foo_ntuple3 (generic function with 1 method)

julia> @descend foo_ntuple3(:a=>1, :b=>2, :c=>3)
foo_ntuple3(t::Pair{Symbol, Int64}...) @ Main REPL[55]:1
1 function foo_ntuple3(t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}}::Pair{Symbol,Int64}...)::Core.Const(nothing)
2 t1::Tuple{Symbol, Symbol, Symbol} = ntuple(i->t[i][1], length(t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})::Core.Const(3))::Tuple{Symbol, Symbol, Symbol}
3 t2::Tuple{Int64, Int64, Int64} = ntuple(i->t[i][2], length(t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})::Core.Const(3))::Tuple{Int64, Int64, Int64}
4 return nothing::Core.Const(nothing)
5 end
Select a call to descend into or ↩ to ascend. [q]uit. [b]ookmark.
Toggles: [w]arn, [h]ide type-stable statements, [t]ype annotations, [s]yntax highlight for Source/LLVM/Native, [j]ump to source always.
Show: [S]ource code, [A]ST, [T]yped code, [L]LVM IR, [N]ative code
Actions: [E]dit source code, [R]evise and redisplay
 • length(t)
   length(t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})
   ntuple(i->t[i][1], length(t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})::Core.Const(3))
   length(t)
   length(t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})
   ntuple(i->t[i][2], length(t::Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}})::Core.Const(3))
   ↩