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