# Tuples, iterators, splats, and type stability

I’m trying understand type stability of functions like

``````f(t::Tuple) = tuple(t...)
g(t::Tuple) = tuple((identity(l) for l in t)...)
``````

`identity` here is meant to be a place-holder for any type stable function. Of the two, `f` is stable, `g` is not:

``````julia> t = (:a, 1, 0.1);
julia> @code_warntype f(t)
Variables
#self#::Core.Compiler.Const(f, false)
t::Tuple{Symbol,Int64,Float64}

Body::Tuple{Symbol,Int64,Float64}
1 ─ %1 = Core._apply_iterate(Base.iterate, Main.tuple, t)::Tuple{Symbol,Int64,Float64}
└──      return %1

julia> @code_warntype g(t)
Variables
#self#::Core.Compiler.Const(g, false)
t::Tuple{Symbol,Int64,Float64}

Body::Tuple{Vararg{Union{Float64, Int64, Symbol},N} where N}
1 ─ %1 = Base.Generator(Main.identity, t)::Base.Generator{Tuple{Symbol,Int64,Float64},typeof(identity)}
│   %2 = Core._apply_iterate(Base.iterate, Main.tuple, %1)::Tuple{Vararg{Union{Float64, Int64, Symbol},N} where N}
└──      return %2
``````

There are two, possibly related, things I don’t understand here. First, what exactly does `tuple(t...)` do? I would have expected this to `iterate` through `t`, and pass the results onto `tuple`. But I guess that’s not what’s happening, since `iterate` doesn’t seem to be type stable:

``````julia> @code_warntype iterate(t)
Variables
#self#::Core.Compiler.Const(iterate, false)
t::Tuple{Symbol,Int64,Float64}

Body::Union{Nothing, Tuple{Union{Float64, Int64, Symbol},Int64}}
1 ─      nothing
│        nothing
│   %3 = (#self#)(t, 1)::Union{Nothing, Tuple{Union{Float64, Int64, Symbol},Int64}}
└──      return %3
``````

So what exactly does splatting do then, and is this explained somewhere in the manual?

Second, for `g`, I’ve come to the conclusion that its type instability is probably caused by the compiler not being able to infer the length of a `Base.Generator{NTuple{N,T}}` from its type. Is there a reason why this is so? Could there be something like a value for `IteratorSize` called `TypeHasLength()` that would be a guarantee that `length(::T) = length(T)` for this type `T`, i.e. the length is inferable from the type? This seems to me like it would make a lot of sense for `Tuple`s, and probably also for a lot of user-defined composite types that have a number of fields, and `iterate` just goes through them. This is actually the original use case that got me thinking about all this: A user-defined, iterable type for which I would like a function like `g` to be type stable.

2 Likes

PS. Derailing my own thread, but in trying to figure this out I’ve really come to think that `map` on generic iterators should return generators. I find it very unintuitive that `map(h, gen)` and `(h(i) for i in gen)` behave so very differently even when `gen` is a generator.

1 Like

From the FAQ:

`...` splits one argument into many different arguments in function calls

That’s really all there is to it: `f(t...)` is simply equivalent to `f(first(t), second(t), ..., last(t))`.

You are correct that one of the issues in your function `g` is that using a generator hides the length of `t` from the compiler, and so Julia must figure out a runtime which method of `tuple` to call and hence it cannot infer the return type of `g`. The other issue is that generators can only have a single `eltype`, so if the elements of `t` have different types then the `eltype` of the generator must be an abstract type which is a supertype of all of the element types.

2 Likes

BTW, Add Iterators.map by tkf · Pull Request #34352 · JuliaLang/julia · GitHub added an uncollected `map` to go with `filter` etc:

``````julia> Iterators.map(sqrt, ntuple(+,5))
Base.Generator{NTuple{5,Int64},typeof(sqrt)}(sqrt, (1, 2, 3, 4, 5))
``````
5 Likes

And what if `t` isn’t a `Tuple`, does it first gets converted to a `Tuple` with something like `tuple(t...)`?

It is iterated. See, among other pages,

https://docs.julialang.org/en/v1/manual/faq/#The-two-uses-of-the-…-operator:-slurping-and-splatting-1

Thanks. Whether it’s iterated is what I meant to ask above as well, but my brain-finger communication failed. I’ve read the pages you link though, and neither one of them really answers the question, e.g. the fact that general iterables go through `iterate` when splatting, but `Tuple`s don’t, is not specified.

Thanks, I wasn’t aware of this. Seems perfect, a reason to look forward to 1.6.

All this seems to point towards a rule of thumb though, that generators are a bad idea when dealing with tuples and tuple-like iterables (e.g. user types that `iterate` over their fields of mixed types), if one wants to maintain type stability.

1 Like

AFAIK `Tuple`s are technically `iterate`d too, it’s just that then the compiler can expand that efficiently.

If you want to find out these things interactively while learning Julia, constructs like

``````julia> Meta.@lower f(x...)
:(\$(Expr(:thunk, CodeInfo(
@ none within `top-level scope'
1 ─ %1 = Core._apply_iterate(Base.iterate, f, x)
└──      return %1
))))
``````

are useful.

Fix for the inference imprecision here (together with some other recent work on master):

https://github.com/JuliaLang/julia/pull/36622

13 Likes

I am officially impressed with the Julia community’s responsivity to feedback.

4 Likes

Should be fixed on master.

10 Likes