Test for a zero-valued Tuple in where statement

I would like to have a special dispatch for the case that a Tuple{} in the type definition is zero (in all entries). For example:

foo(a::AbstractArray{T, N}) where {N, T<:Tuple{zeros(Int,N)...}}

but get an error since N when used inside the where statement is of type ::TypeVar.
Is there a way to convert this into a value?
I also tried

foo(a::AbstractArray{T, N}) where {N, T<:Tuple{zeros(Int,Val(N))...}} = a

which gives a somewhat strange error:

ERROR: UndefVarError: x not defined
 [1] Val{N}()
   @ Base .\essentials.jl:712
 [2] Val(x::TypeVar)
   @ Base .\essentials.jl:714
 [3] top-level scope
   @ REPL[91]:1

Note that

foo(a::AbstractArray{T,N}) where {N,T<:Tuple{zeros(Int,3)...}} = a

works just fine.

1 Like

More notably is that this crashed with segmentation fault Julia 1.9 (bug filled here).

Concerning your question: I don’t think you can do that. If you really need that behavior you probably need to wrap the tuple in a custom struct that is type-parameterized for that specifically.

1 Like

Here is what I don’t understand: I don’t see how you can make the simplified example

foo(a::AbstractArray{T,1}) where {T<:Tuple{zeros(Int,1)...}} = a

@show foo(Tuple{0}[]) # works
@show foo(Tuple{0}[()]) # fails with
# MethodError: Cannot `convert` an object of type
#   Tuple{} to an object of type
#   Tuple{0}

(which compiles) make to work for a non empty vectors. Ideas anyone?

Tuple{0} is a “valid” type just like Array{1.2,3//4} is a “valid” type. 0, 1.2, and 3//4 are all isbits so are valid type parameters. This does not mean they actually result in “well-formed” types.

So while you can instantiate an array via Tuple{0}[] or Array{Tuple{0}}(undef,n), there is literally no element that you can insert into them because there are no instances of a Tuple{0} that can exist.

You can have a value (0,0) with typeof((0,0)) == Tuple{Int,Int}, however, which is probably closer to what you’re actually dealing with. Notice the parentheses () for values and braces {} for types.

To the original question: you cannot check for a value (including zero) at the type level. You can create Val{0}(), which is a value of type Val{0}, but that will not help you here and Vals aren’t useful for much except dispatch. It’s a bit unclear exactly what is being asked because it’s unclear where types versus values are intended.

You can write

foo(a::AbstractArray{<:Tuple{Vararg{Any,N}},N}) where N = all(Base.Fix1(all,iszero), a) ? foo_if_all_zero(a) : foo_else(a)

# or, if you don't care about the length of the tuple versus the dimension of the array
foo(a::AbstractArray{<:Tuple}) = all(Base.Fix1(all, iszero), a) ? foo_if_all_zero(a) : foo_else(a)

and proceed to define the foo_if_all_zero and foo_else function specifically. But it’s unclear if this is what you’re after. Why should the length of the tuple match the dimension of the array?


What are you trying to dispatch on?

It sounds like you mean to dispatch for

julia> t = (0, 0, 0, 0)
(0, 0, 0, 0)

julia> typeof(t)
NTuple{4, Int64}

julia> Tuple{Int, Int, Int, Int} == NTuple{4, Int}

As shown above the type of t is NTuple{4, Int64}, not NTuple{4, Val{0}}. For that we would need the following.

julia> t2 = Val.((0,0,0,0))
(Val{0}(), Val{0}(), Val{0}(), Val{0}())

julia> typeof(t2)
NTuple{4, Val{0}}

Then we could do dispatch as follows.

julia> foo(a::AbstractArray{T,N}) where {N, T <: NTuple{N,Val{0}}} = 5
foo (generic function with 1 method)

julia> foo([t2 ;;;;])

If you wanted a more flexible version where T could be an NTuple with any number of Val(0), you could do

julia> foo(a::AbstractArray{T,N}) where {N, N2, T <: NTuple{N2,Val{0}}} = N2
foo (generic function with 2 methods)

julia> foo([Val.((0,)) ;;;;])

julia> foo([Val.((0,0)) ;;;;])

julia> foo([Val.((0,0,0)) ;;;;])

However, my suspicion is that you really want to dispatch on something like (0,0,0,0). We can do this with two definitions. The first checks the array to make sure it consists of tuples of a single value.

julia> foo(a::AbstractArray{T,N}) where {N, T <: NTuple{N}} = foo(a, Val(only(unique(only(unique(a))))))
foo (generic function with 4 methods)

julia> foo(a::AbstractArray{T,N}, ::Val{0}) where {N, T <: NTuple{N}} = println("This $(N)D array is filled with $N-Tuples filled with zeros")
foo (generic function with 4 methods)

julia> foo([ (0,) ])
This 1D array is filled with 1-Tuples filled with zeros

julia> foo([ (0,0) ;; ])
This 2D array is filled with 2-Tuples filled with zeros

julia> foo([ (0,0,0) ;;; ])
This 3D array is filled with 3-Tuples filled with zeros

julia> foo([ (0,0,0,0) ;;;; ])
This 4D array is filled with 4-Tuples filled with zeros

julia> foo([ (0,0,0) ;;;; ])
ERROR: MethodError: no method matching foo(::Array{Tuple{Int64, Int64, Int64}, 4})

julia> foo([ (0,0,0,1) ;;;; ])
ERROR: ArgumentError: Collection has multiple elements, must contain exactly 1 element

julia> foo([ (1,1,1,1) ;;;; ])
ERROR: MethodError: no method matching foo(::Array{NTuple{4, Int64}, 4}, ::Val{1})

Honestly though, you probably should just write an if statement rather than using multiple dispatch to do this.


I just found a possible solution, which seems pretty much a simplified version of your Vararg suggestion

foo(a::AbstractArray{T, N}) where {N, T<:NTuple{N,0}} = a

I think this should do.
The point really is that I would like to treat a CircShiftedArray with shift zero at the compiler-level as an ordinary array, to speed up things. So the compiler should be able to figure out that there is a part of the type signature having the Shfit: Tuple{0,0,0}.

There is no value of type NTuple{N, 0} where N. Thus, the array type you’re after can only ever be empty so isn’t very interesting. You are trying to identify the value (0,0,0) which has type NTuple{3,Int}. But the value (1,2,-3) also has this same type. You cannot distinguish among them via dispatch.

Honestly, I don’t expect a massive gain from using the special case of a zero offset. I’d start without it. You can make an if all(iszero, offsets) (where offsets is a value of type NTuple{N,Int}) branch to handle this special case inside your function (or have it call a different function) if you do find it to be a useful optimization.


Sorry for bothering you: my adapted test is

foo(a::AbstractArray{T, 1}) where {T<:NTuple{1,0}} = a

@show foo(NTuple{1, 0}[]) # works
@show foo(NTuple{1, 0}[()]) # fails with
# MethodError: Cannot `convert` an object of type
#   Tuple{} to an object of type
#   Tuple{0}

and I’m still failing to see how to create a non empty vector here. Could you show us how to do this, please?

Sorry, I think the “simplified example” I was trying to make up was a bit over-simplified.
Here is the code which I put in the package:

split_array_broadcast(bc::CircShiftedArray{N,S}, noshift_rng, shift_rng)  where {N,S<:NTuple{M,0}} where {M}= @view bc.parent[noshift_rng...]

It is supposed to be triggered for example for a::CircShiftArray{2,Tuple{0,0}} but not for a::CircShiftArray{2,Tuple{0,1}}.
Does this make sense now?

Have you managed to make a instance of CircShiftArray{2,Tuple{0,0}} with >0 elements yet? If so, please provide example code for that and a function call for split_array_broadcast.

This does not work. There is no way to create an instance of Tuple{0,0,0}.

julia> (0,0,0) isa Tuple{0,0,0}

You can do one of these.

julia> (Val(0),Val(0),Val(0)) isa Tuple{Val{0},Val{0},Val{0}}

julia> Val((0,0,0)) isa Val{(0,0,0)}
1 Like

Okay. If you’re using the type Tuple{X,Y} to hardcode the offsets then you can get away with this. StaticArrays.jl uses this same technique to store array sizes in the type domain. I believe what you wrote (NTuple{N,0} where N) will work for your purposes, i.e., identifying the type or type parameter Tuple{0,0,...} (of which no instance can exist, but the type itself is valid).

I still have some skepticism that this provides a significant performance benefit over just holding the offset as a value of type NTuple{N,Int}. But that comes down to how much one considers “significant” and the amount of extra code they’re willing to produce. If your offsets are not known at compile time, this will perform much worse than a value-based solution because it will necessitate dynamic dispatch. It will also result in a separate function being compiled for each unique offset, which may quickly get out of hand.

You can simplify further as follows.

julia> bar(a::CircShiftedArray{N,NTuple{N,0}}) where N = 1
bar (generic function with 1 methods)

julia> bar(a::CircShiftedArray{N,<: Tuple}) where N = 0
bar (generic function with 2 methods)

julia> bar(CircShiftedArray{2,Tuple{0,1}}())

julia> bar(CircShiftedArray{2,Tuple{0,0}}())
1 Like

Now I think about it, why is Tuple{size_tuple...} used there instead of Val{size_tuple}?

Great. Thanks mkitti for the suggestion. Works nicely.

This is above my pay grade, but I’m guessing so that they can do things like this

julia> Tuple{1,2} <: Tuple{1,N} where N

julia> Val{(1,2)} <: Val{(1,N)} where N
ERROR: TypeError: in Type, in parameter, expected Type, got a value of type Tuple{Int64, TypeVar}

Obviously that specific pattern probably never appears, but I’m sure there are cases where something like it is useful.

1 Like