Quinn's Quibbles: How empty tuples in Julia get condensed down

A tuple is an immutable data type in Julia. (1, 2, 3) is a tuple, and its type is Tuple{Int64,Int64,Int64}. You can't change it by doing something nifty like (1,2,3)[1] = "fish", although you can make a brand new ("fish", 2, 3) from whole cloth if you wanted to.

() is also a tuple, although it's an empty tuple — the Haskell community sometimes calls this a coin. Its type is (wait for it) Tuple{}.

So what do you think the type of (()) would be? Tuple{Tuple{}}? Well...

julia> ()
()

julia> (())
()

julia> ((()))
()

julia> (((())))
()

julia> typeof( (()) )
Tuple{}

Not exactly. Julia condenses down empty tuples, unless you twist its arm a bit to give you what you actually typed:

julia> ()
()

julia> ((),)
((),)

julia> (((),))
((),)

julia> (((),),)
(((),),)

julia> typeof( ((),) )
Tuple{Tuple{}}

This would mostly be fine, except that tuples are intimately tied to function arguments with the f(x...) syntax. The tuples with this syntax are not condensed by default in the same way:

julia> f(x) = x
f (generic function with 1 method)

julia> f( () )
()

julia> f( (()) )
()

julia> g(x...) = x
g (generic function with 1 method)

julia> g( () )
((),)

julia> g( (()) )
((),)

So why don't functions condense empty tuples the same way that (()) does? Isn't that a bit unintuitive?

Well, slightly, yes. But when we're typing out nested tuples, it's easy to force them if we have to – we just do ((),). If we wanted our f(x...) = x to return () instead of ((),) when we pass it with f( () ), we would need to also provide some way to force f to not condense it when we have to.

That seems like a lot of work, for very little gain – in fact, net negative gain, because I'm pretty sure the average Julia coder isn't anal enough to be messing around with empty tuples all that often! 😂