https://docs.julialang.org/en/v1/base/base/#...
Is there a way to know what data types the splat operator can be used with? Through experimentation, I've used it with:
Are there any more?
I think it just calls iterate
, so anything iterable
Forgive my newbish question, but is there a type that contains all iterable things?
there is not
Thanks.
G Gundam has marked this topic as resolved.
Many (most?) things with multiple elements probably are iterable though!
One key thing to keep in mind with splatting though is AFAIU performance may not be great with things where either (A) the collection has many elements and/or (B) the compiler can't tell how many elements are in a collection from its type
so splatting Tuples is generally fine, but splatting arrays may not be ideal
I'll keep that in mind. I tend to use splatting when passing kwargs, and it's usually not in loops. A lot of times, it's in a configuration-esque context where I call something like somefunction(arg1, arg2; kwargs...)
julia> a = [1,2,3]
3-element Vector{Int64}:
1
2
3
julia> Meta.@lower tuple(a...)
:($(Expr(:thunk, CodeInfo(
@ none within `top-level scope`
1 ─ %1 = Core._apply_iterate(Base.iterate, tuple, a)
└── return %1
))))
help?> ...
search: ...
...
The "splat" operator, ..., represents a sequence of arguments. ... can be used in function definitions, to indicate that the
function accepts an arbitrary number of arguments. ... can also be used to apply a function to a sequence of arguments.
also note that it really does call iterate
, which affects stateful iterators weirdly:
julia> itr = Iterators.Stateful(1:10)
Base.Iterators.Stateful{UnitRange{Int64}, Union{Nothing, Tuple{Int64, Int64}}}(1:10, (1, 1))
julia> tuple(itr...)
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
julia> tuple(itr...)
()
Is there any information about when the compiler knows how many elements are in the iterable? Is it only able to reliably do so for tuples? It currently seems like the only widely-known way to control this for custom types is to always lower the iterable to a Tuple
, and splat that.
A general iterator, not really. At most it can know if it has finite length or not. Maybe there’s things like FixedSizeArrays that the compiler can reason about more effectively. Otherwise it’s just compile time knowledge from tuples and or sizes embedded in the type system.
But, with enough information the compiler can probably const prop things and actually compute the total iteration length. But it’s hard to give assurances I’d say
Lowering a generic iterator to a tuple simply moves the problem of figuring out how many things are in it to another point in the program.
The compiler will try to unroll iteration up to max_tuple_splat
elements, which is 32 by default, so even quite complex examples like the following will "just work":
julia> code_typed((typeof((; a = 1, b = 2, c = 3)),); optimize=false) do nt
tuple(Iterators.flatten((1:3, zip((2, 4), (i^2 for i in 5:6)), nt))...)
end
1-element Vector{Any}:
CodeInfo(
1 ─ %1 = Main.tuple::Core.Const(tuple)
│ %2 = Base.Iterators.flatten::Core.Const(Base.Iterators.flatten)
│ %3 = (1:3)::Core.Const(1:3)
│ %4 = Main.zip::Core.Const(zip)
│ %5 = Core.tuple(2, 4)::Core.Const((2, 4))
│ %6 = Main.:(var"#26#28")::Core.Const(var"#26#28")
│ (#26 = %new(%6))::Core.Const(var"#26#28"())
│ %8 = #26::Core.Const(var"#26#28"())
│ %9 = (5:6)::Core.Const(5:6)
│ %10 = Base.Generator(%8, %9)::Core.Const(Base.Generator{UnitRange{Int64}, var"#26#28"}(var"#26#28"(), 5:6))
│ %11 = (%4)(%5, %10)::Core.Const(zip((2, 4), Base.Generator{UnitRange{Int64}, var"#26#28"}(var"#26#28"(), 5:6)))
│ %12 = Core.tuple(%3, %11, nt)::Core.PartialStruct(Tuple{UnitRange{Int64}, Base.Iterators.Zip{Tuple{Tuple{Int64, Int64}, Base.Generator{UnitRange{Int64}, var"#26#28"}}}, @NamedTuple{a::Int64, b::Int64, c::Int64}}, Any[Core.Const(1:3), Core.Const(zip((2, 4), Base.Generator{UnitRange{Int64}, var"#26#28"}(var"#26#28"(), 5:6))), @NamedTuple{a::Int64, b::Int64, c::Int64}])
│ %13 = (%2)(%12)::Core.PartialStruct(Base.Iterators.Flatten{Tuple{UnitRange{Int64}, Base.Iterators.Zip{Tuple{Tuple{Int64, Int64}, Base.Generator{UnitRange{Int64}, var"#26#28"}}}, @NamedTuple{a::Int64, b::Int64, c::Int64}}}, Any[Core.PartialStruct(Tuple{UnitRange{Int64}, Base.Iterators.Zip{Tuple{Tuple{Int64, Int64}, Base.Generator{UnitRange{Int64}, var"#26#28"}}}, @NamedTuple{a::Int64, b::Int64, c::Int64}}, Any[Core.Const(1:3), Core.Const(zip((2, 4), Base.Generator{UnitRange{Int64}, var"#26#28"}(var"#26#28"(), 5:6))), @NamedTuple{a::Int64, b::Int64, c::Int64}])])
│ %14 = Core._apply_iterate(Base.iterate, %1, %13)::Core.PartialStruct(Tuple{Int64, Int64, Int64, Tuple{Int64, Int64}, Tuple{Int64, Int64}, Int64, Int64, Int64}, Any[Core.Const(1), Core.Const(2), Core.Const(3), Core.Const((2, 25)), Core.Const((4, 36)), Int64, Int64, Int64])
└── return %14
) => Tuple{Int64, Int64, Int64, Tuple{Int64, Int64}, Tuple{Int64, Int64}, Int64, Int64, Int64}
All this requires is for the compiler to be able to do (partial) constant propagation through iterate
and eventually reach nothing
. The issues with arrays is just that the size is mutable and as such the compiler is limited about what it can reason about their size.
I'd imagine it can reason about the lengths of StaticArray
s as well (which I suppose are kind of tuples under the hood anyways)
If you define a length method that returns a constant number, Julia is pretty good with constant propagation/evaluation, so you can use most iterables with a constant length.
IMO the bigger issues are 1) collections that does not have any compile time length info are splattable, laying a compilation trap for users, and 2) for collections whose length is part of the type, it's tricky to consistently write code that doesn't cause a codegen explosion by creating lots of Intermediate types.
Last updated: Oct 18 2025 at 04:39 UTC