Stream: helpdesk (published)

Topic: ✔ Question about splat ... operator


view this post on Zulip G Gundam (Oct 07 2025 at 20:40):

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?

view this post on Zulip Andy Dienes (Oct 07 2025 at 20:53):

I think it just calls iterate, so anything iterable

view this post on Zulip G Gundam (Oct 07 2025 at 20:54):

Forgive my newbish question, but is there a type that contains all iterable things?

view this post on Zulip Andy Dienes (Oct 07 2025 at 20:55):

there is not

view this post on Zulip G Gundam (Oct 07 2025 at 20:55):

Thanks.

view this post on Zulip Notification Bot (Oct 07 2025 at 20:55):

G Gundam has marked this topic as resolved.

view this post on Zulip Brenhin Keller (Oct 07 2025 at 21:43):

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

view this post on Zulip Brenhin Keller (Oct 07 2025 at 21:43):

so splatting Tuples is generally fine, but splatting arrays may not be ideal

view this post on Zulip G Gundam (Oct 07 2025 at 21:48):

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...)

view this post on Zulip jar (Oct 07 2025 at 22:01):

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
))))

view this post on Zulip jar (Oct 07 2025 at 22:02):

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.

view this post on Zulip Andy Dienes (Oct 08 2025 at 01:42):

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...)
()

view this post on Zulip Jakob Peters (Oct 08 2025 at 02:57):

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.

view this post on Zulip cschen (Oct 08 2025 at 10:04):

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.

view this post on Zulip Simeon Schaub (Oct 08 2025 at 11:14):

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.

view this post on Zulip Brenhin Keller (Oct 08 2025 at 18:51):

I'd imagine it can reason about the lengths of StaticArrays as well (which I suppose are kind of tuples under the hood anyways)

view this post on Zulip Jakob Nybo Nissen (Oct 11 2025 at 11:41):

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