Type declaration in function arguments: Allow adjoints and column vectors?

I have a function that nominally accepts only 2-dimension arrays as input:

julia> function foo(x::Array{Float64, 2})
           # Stuff
           return nothing
       end
foo (generic function with 1 method)

julia> y = rand(5, 6)
5×6 Array{Float64,2}:
 0.818613  0.402175  0.141845  0.649995  0.622611  0.432664
 0.368095  0.196203  0.235136  0.657062  0.281461  0.603284
 0.962769  0.335103  0.875451  0.587475  0.620739  0.393967
 0.957616  0.273278  0.740744  0.14692   0.792191  0.779481
 0.95631   0.815702  0.961613  0.49849   0.188148  0.96158

julia> foo(y) # No error

But I sometimes want to call this function on the transpose of an array. This doesn’t work:

julia> foo(y')
ERROR: MethodError: no method matching foo(::LinearAlgebra.Adjoint{Float64,Array{Float64,2}})
Closest candidates are:
  foo(::Array{Float64,2}) at none:1
Stacktrace:
 [1] top-level scope at none:1

Instead, I have to do

julia> foo(permutedims(y)) # No error

Likewise, sometimes my array is just a column vector, and in such cases I would like it to be treated as a 2D array whose second dimension is 1:

julia> z = rand(5)
5-element Array{Float64,1}:
 0.373288007992006
 0.971884387492479
 0.8786393449999188
 0.9435088229078494
 0.570539122742798

julia> foo(z)
ERROR: MethodError: no method matching foo(::Array{Float64,1})
Closest candidates are:
  foo(::Array{Float64,2}) at none:1
Stacktrace:
 [1] top-level scope at none:1

julia> foo(reshape(z, 5, 1)) # No error

Now, I understand that I can explicitly allow adjoints and column vectors by modifying the function like so:

using LinearAlgebra

function foo(x::Union{Array{Float64,2},
                      Adjoint{Float64,Array{Float64,2}},
                      Array{Float64,1}})
    if typeof(x) == Array{Float64,1}
        x = reshape(x, :, 1)
    end
    return nothing
end

(In my real function, there are loops over both dimensions of x, so the reshaping is necessary.)

But this is unsightly, and I am wondering if there is a better way to do this. Is there a type declaration that is equivalent to the following statement?

  • "x should be an 2-dimensional array, or something that can be unambiguously interpreted as such"
1 Like

What is happening is that you are restricting foo to only accept Array{Float64, 2} types. The output of y' is not Array{Float64, 2}. Perhaps you should define foo to accept a subtype of DenseArray, AbstractArray or do not specify any type at all. Or take a look at Parametric Types and Parametric Methods in the documentation to specify your last condition.

2 Likes

To expand upon the previous answer, usually you want to leave the argument types as generic as possible. So, in this case something like the following might be a good option:

function foo(x::AbstractArray{T, 2}) where T
    return 1
end

function foo(x::AbstractVector{T}) where T
    return foo(reshape(x, :, 1))
end

You can put a type constraint on T if you need the entries to be real numbers:

function foo(x::AbstractArray{<:Real, 2})
    return 1
end

function foo(x::AbstractVector{<:Real})
    return foo(reshape(x, :, 1))
end
3 Likes

Thank you both for the helpful responses. I have been playing around with types and I am having trouble wrapping my head around this:


julia> Float64 <: Real
true

julia> Array{AbstractFloat, 1} <: AbstractArray{Real, 1}
false

What is the intuition behind this? To me, it seems like if Float64 is a subtype of Real, and Array a subtype of AbstractArray, then the second statement should hold.

Edit: The solution is to use

julia> Array{AbstractFloat, 1} <: AbstractArray{<:Real, 1}
true

I guess the <: operator doesn’t “distribute” to the inside of the braces.

Julia types are invariant on their type parameters. That means, T<:S implies neither Vector{T} <: Vector{S} nor Vector{T} >: Vector{S}.
Rationale: Vector{Real} <: Vector{Number} makes sense only if we consider vectors as values. “An iterable which produces real values” is a kind of “an iterable which produces numeric values”. But vectors can also be viewed as containers. “A container which can store any real value” is not a kind of “a container which can store any numeric value”. In fact, the opposite holds: anywhere a Vector{Real} is suitable as a container, a Vector{Number} must be as well. That leads to conclusion that neither of Vector{Real} and Vector{Number} can be viewed as a supertype of another.

3 Likes

OK, I think I’m getting this. So S <: T & VectorA <: VectorB implies VectorA{S} <: VectorB{<:T}, and does the converse hold as well?

Yes, that holds.