Usage of Correct Types in Array

Hi,

I want to write a function that can take matrix composing of both int and float and return if it is a upper triangle matrix or not:

function isUpperTriangle(A::Array{Number, 2}, tol::Number=1e-3)::Bool
    for i in CartesianIndices(A)
        if i[1] > i[2]
            if A[i[1], i[2]] > tol
                return false
            end
        end
    end
    true
end

However, when I test it out like:

tt = [ 1 2 3; 0 3 4; 0 0 5]
isUpperTriangle(tt) # Should return true

it returns:

julia> isUpperTriangle(tt) # Should return true
ERROR: MethodError: no method matching isUpperTriangle(::Array{Int64,2})
Closest candidates are:
  isUpperTriangle(::Array{Number,2}) at REPL[1]:2
  isUpperTriangle(::Array{Number,2}, ::Number) at REPL[1]:2
Stacktrace:
 [1] top-level scope at REPL[3]:1

If I change the type annotation from Number to <:Number then it works fine. The thing I don’t understand is I can use the annotation like this:

function test_fun(x::Number)
    println(x)
end

and that accepts both Int and Float fine. Why is that?

Thank you!

Types in Julia are invariant. Thus x{A}<:x{B} implies A===B. https://stackoverflow.com/questions/8481301/covariance-invariance-and-contravariance-explained-in-plain-english is a pretty good rundown of the difference.

2 Likes

As mentioned, parametric types “Type{T}” with different values for T are never consided the same type.

What you can do is explicitely initiate your input array as:

Array{Number,2}([ 1 2 3; 0 3 4; 0 0 5])

The better approach is to define isUpper using <:Number

This is 8x slower on my laptop, so I would not recommended this in place of just doing things the proper way.

1 Like

To clarify, the proper way is the following:

function isUpperTriangle(A::Array{<:Number, 2}, tol::Number=1e-3)::Bool
    for i in CartesianIndices(A)
        if i[1] > i[2]
            if A[i[1], i[2]] > tol
                return false
            end
        end
    end
    true
end

julia> tt = [ 1 2 3; 0 3 4; 0 0 5];

julia> isUpperTriangle(tt)
true

The only difference is the Array{<:Number, 2} in the function signature instead of Array{Number, 2}.


By the way, it is often useful to write methods that are as general as possible (maybe not in this case, but that’s up to you). You could get the same functionality with this more general function:

function isUpperTriangle(A::AbstractMatrix, tol = 1e-3)
    for i in CartesianIndices(A)
        if i[1] > i[2]
           if A[i] > tol
               return false
           end
       end
   end
   true
end

Now this still works:

julia> isUpperTriangle(tt)
true

But now so does any matrix type, as long as tol was something comparable with the elements of A. Note below that tt' (tt transpose) is not an Array, but the function still works.

julia> isUpperTriangle(tt')
false

The following will also work

julia> isUpperTriangle(Char.(tt), Char(1))
true

julia> isUpperTriangle(Char.(tt'), Char(1))
false
1 Like

This is because Number is an abstract type and Float64 <: Number. Therefore a function written for x::Number can be compiled for Float64. However, Vector{Number} is a concrete type, and Vector{Float64} is therefore not a subtype of Vector{Number}. Since it is not a subtype, a function written for Vector{Number} cannot accept Vector{Float64}. This is why you must specify Vector{<:Number}.

By the way, this can be understood by realizing that it is possible to promote an array of Float64s to an array of Numbers, but it is never possible to promote a single Float64 to a Number. If a type can “exist” as an object, it is necessarily concrete.

julia> typeof(Vector{Number}([1.0,2.0]))
Array{Number,1}

julia> typeof(Number(1.0))
Float64
1 Like