In functions, you can put restrictions on the types of arguments like so:

function f(a::Array{T, 3}) where {T<:Real}
[ Function Body ...]
end

Can you do something similar for type parameters that are not types, but isbits instances, like integers and floats? For example, I would have something like:

function f(a::Array{T, N}) where {T<:Real, N > 2}
[ Function Body ...]
end

julia> @generated function f(a::Array{T,N}) where {T <: Real, N}
if N > 2
:(println("Hello World"))
else
:(throw(MethodError(f,(a,))))
end
end
f (generic function with 1 method)
julia> f(zeros(1))
ERROR: MethodError: no method matching f(::Vector{Float64})
Closest candidates are:
f(::Array{T, N}) where {T<:Real, N}
@ Main REPL[27]:1
Stacktrace:
[1] macro expansion
@ .\REPL[27]:1 [inlined]
[2] f(a::Vector{Float64})
@ Main .\REPL[27]:1
[3] top-level scope
@ REPL[28]:1
julia> f(zeros(1,2))
ERROR: MethodError: no method matching f(::Matrix{Float64})
You might have used a 2d row vector where a 1d column vector was required.
Note the difference between 1d column vector [1,2,3] and 2d row vector [1 2 3].
You can convert to a column vector with the vec() function.
Closest candidates are:
f(::Array{T, N}) where {T<:Real, N}
@ Main REPL[27]:1
Stacktrace:
[1] macro expansion
@ .\REPL[27]:1 [inlined]
[2] f(a::Matrix{Float64})
@ Main .\REPL[27]:1
[3] top-level scope
@ REPL[29]:1
julia> f(zeros(1,2,3))
Hello World
julia> f(zeros(1,2,3,4))
Hello World

T<:Real seems to imply T is substituted at runtime e.g. Int then dispatchesInt<:Real, but the latter half does not happen. I’m not entirely certain because I can’t remember a way to interact with it, but it’s more like T is an instance of TypeVar so previous loophole attempts to involve more generic method calls failed. So, you’re restricted to <:, a built-in function you cannot change, and >:, which is a generic function but just forwards to <:. I’m not actually sure if that generic function itself is present in where clauses; as much as I change Base.:>:, it does not interfere with method definitions.

You also wouldn’t want a redefinable generic method to complicate dispatch anyway. For one, redefining >(::Int, ::Int) changes method dispatch and invalidates things. For another, it is not trivial to infer subsets of instances like you can with types. You can tell T<:Int takes priority over T<:Real because there is a scattered chain of <: declarations that lets the operation Int<:Real return true. How would you tell that N > 5 is a subset of N > 2? We can tell at a glance because we know the intended meaning, but it’s another matter to communicate that to the method table. Brute force way is to evaluate over every possible instance and count. We could make an associated method for > specifically for quickly calculating subsets, but then it either relies on > never changing, which is not very generic at all, or involves nightmarish maintenance of distant places.

A comparatively simple way to establish priority is to list conditions in order, and while we can’t impose order on methods, we can do that with if-statements. I think constant folding can eliminate branches too, though generated functions are more of a guarantee.

You can verify validity (or do other N-dependent logic) within the function body. The branches are compiled away, at least in simple cases, you don’t need a @generated function for that.

julia> function f(a::Array{<:Real,N}) where {N}
(N <= 2) && error("Need more N")
println("Valid N")
end
f (generic function with 1 method)
julia> f(zeros(1, 1))
ERROR: Need more N
Stacktrace:
[1] error(s::String)
@ Base ./error.jl:35
[2] f(a::Matrix{Float64})
@ Main ./REPL[30]:2
[3] top-level scope
@ REPL[31]:1
julia> f(zeros(1, 1, 1))
Valid N
julia> @code_llvm f(zeros(1, 1))
; @ REPL[30]:1 within `f`
; Function Attrs: noreturn
define void @julia_f_316({}* noundef nonnull align 16 dereferenceable(40) %0) #0 {
top:
; @ REPL[30]:2 within `f`
call void @j_error_318({}* inttoptr (i64 139717740904624 to {}*)) #0
unreachable
}
julia> @code_llvm f(zeros(1, 1, 1))
; @ REPL[30]:1 within `f`
define void @julia_f_319({}* noundef nonnull align 16 dereferenceable(40) %0) #0 {
top:
; @ REPL[30]:3 within `f`
call void @j_println_321({}* inttoptr (i64 139717734901016 to {}*)) #0
ret void
}

Notice that the LLVM is different for the two calls and does not have any branches. It directly calls error when you give a 2D array and println when you give a 3D array.