Tuple{Vararg{T, N}} where N >= 1

It can be extremely convenient to dispatch on the types of arguments in a tuple, creating an alias for tuples of a type. However, a zero-length tuple will match a Vararg Tuple of any type, which can create issues with dispatch and type piracy. How can I impose the restriction that the vararg tuple have at least one element?

MWE:

abstract type AbstractMyType end 
struct MyType <: AbstractMyType  
    x
end 

const InformalGroupedType = Tuple{Vararg{AbstractMyType}}

do_something(arg::InformalGroupedType) = println("did something") 

my_informal_grouped_type = 1:3 .|> MyType |> Tuple 

do_something(my_informal_grouped_type) 

Unfortunately, a zero-length tuple can match the above alias

typeof(Tuple(){}) <: InformalGroupedType # evaluates to true 

Is there any way to impose the restriction

# this is not syntactically valid.  Is there a syntax for this idea? 
const InformalGroupedType = Tuple{Vararg{AbstractMyType, N}} where N >= 1 

?

This is causing me problems when I extend the show function to my analogue of InformalGroupedType.
The big problem here is that dispatching an existing function on a Tuple{Vararg{T}} is implicitly type piracy even if T is a type I created. So, unless there is a way to restrict the Vararg to length greater than 0, one ought never extend existing functions to Vararg tuples of user defined type, which would be disappointing.

You cannot - define a method that takes one argument, as well as the Vararg.

Not at the moment, no. There are various versions of this idea and lots of people look for something like this (most often in the context of “some type parameter must be in some range of values”). You’ll probably find some threads about this here already.

2 Likes

Thanks for the information, and that’s too bad.

Aliasing Vararg Tuples of my type and dispatching on the alias is too convenient for me to stop using it, but I will have to be careful that implicit type piracy does not cause problems.

Sure:

const InformalGroupedType = Tuple{AbstractMyType, Vararg{AbstractMyType}}
5 Likes

Your answer is correct, prior answer was wrong.

Prior response has it right.
There is currently no way to impose that Vararg is nonzero, so typeof(Tuple{}()) <: Tuple{Vararg{T}} where T <: WhateverType
for any type WhateverType, which means writing methods of existing functions on a vararg tuple type is implicitly type piracy

I am an idiot, I misread your answers as simply dropping the N from the Vararg argument. Your answer is correct, the previous answer was incorrect. Thank you.

2 Likes

I’m probably missing something obvious, but why would this restriction solve your type piracy issue?

Even if you own type T, Tuple{Vararg{T}} includes the empty tuple type Tuple{}, which is not a type owned by the package that provides type T. Vararg{T} represents zero or more instances of type T.

If an already existing function from a different package, f, doesn’t have a method with signature f(::Tuple{}), then a package providing a method f(::Tuple{Vararg{T}}) would implicitly define it for Tuple{} arguments.

The solution is to dispatch on Tuple{T, Vararg{T}} because it guarantees at least one element of type T is present in the tuple and is therefore never empty.

Methods like this can also be the source of method ambiguities, so it’s useful to use this construct in various places.

4 Likes

I hope I understand your question. Forgive me if I don’t.

EDIT: and the above poster beat me to it, but I’ll dogpile my answer here just in case it helps someone see more clearly.

An example of the issue is that Base.:+(::Vararg{MyType}) matches

Base.:+() # this is piracy! MyType is not involved!
Base.:+(::MyType)
Base.:+(::MyType, ::MyType)
Base.:+(::MyType, ::MyType, ::MyType)
# etc

whereas the definition Base.:+(::MyType, ::Vararg{MyType}) would match all of these except the problematic Base.:+().

The same thing happens with Tuple{Vararg}. () isa Tuple{Vararg{MyType}} and also () isa Tuple{Vararg{AnyOtherType}}. It’s ambiguous what the eltype of an empty Tuple should be.

So the takeaway is that a purely-Vararg argument/tuple (or NTuple{N} where N if N could be 0) does not actually provide the disambiguation required to avoid piracy. You need another identifiable argument to the method/tuple to prevent accidental piracy in the empty case.

3 Likes

Thanks for the explanation, @brainandforce and @mikmoore! I’m pretty sure I now better understand the point @croberts was making.

It seems Julia has some built-in safety for Vararg{T, N} when N == 0 as it is indeed inherently ambiguous about T. (Edit: see also the documentation.)

julia> f(a::Vararg{Int}) = 1
f (generic function with 1 method)

julia> f(a::Vararg{Float64}) = 2
f (generic function with 2 methods)

julia> f()
ERROR: MethodError: f() is ambiguous.

Candidates:
  f(a::Float64...)
    @ Main REPL[2]:1
  f(a::Int64...)
    @ Main REPL[1]:1

Possible fix, define
  f()

But I struggle to see how this would lead to type piracy (unless you forcefully define f() “for your own type T”). Also, the only situation I can imagine the ambiguity manifesting naturally is when splatting a Tuple{T} which turns out to be empty.

julia> function g(x)  # ::Tuple{Int}  but not explicitly as !( () isa Tuple{Int})
           if x > 0
               return (1, 2)
           elseif x < 0
               return (3)
           else
               return ()
           end
       end
g (generic function with 1 method)

julia> f(g(1)...)
1

julia> f(g(0)...)
ERROR: MethodError: f() is ambiguous.

(Edit: In the previous posts we’re using Tuple{Vararg{T}} instead of just Vararg{T}, but I don’t think this matters.)