Vector{Real}, Vector{Float64} MethodError

Hi,
the following code runs fine:

function h(x::Real)
    println(x)
end

function j(x::Float64)
    println(x)
end

arr2 = [1.0, 2.0]
h(arr2[1])
j(arr2[1])

giving me

1.0
1.0

But this code

function f(v::Vector{Real})
    println(v)
end


function g(v::Vector{Float64})
    println(v)
end

arr = [[1.0, 1.0], [1.0, 2.0]]
g(arr[1])
f(arr[1])

errors with

[1.0, 1.0]

MethodError: no method matching f(::Array{Float64,1})
Closest candidates are:
f(!Matched::Array{Real,1}) at In[2]:2

Stacktrace:
[1] top-level scope at In[2]:12

Why that and what am I doing wrong? I thought you can put Float64 everywhere where you can put Real in…

That is not true. Specifically

julia> Float64<:Real
true

but

julia> Vector{Float64}<:Vector{Real}
false

This property of the type system is explained in the manual:
https://docs.julialang.org/en/v1/manual/types/index.html#Parametric-Composite-Types-1
Especially notice the warning there.

2 Likes

You probably want

function f(v::Vector{<:Real})
    println(v)
end

which is (roughly) shorthand for

function f(v::Vector{T}) where {T<:Real}
    println(v)
end
4 Likes

@aharoun Thanks for the quick answer!
So changing the code to (as mentioned in the manual)

function f(v::Vector{<:Real})
    println(v)
end
f(arr[1])

works and gives

[1.0, 1.0]

as answer. So far, so good! @tkoolen Thanks for the answer as well! I just typed this in as I saw your post.

I read through the manual and understand the performance issues mentioned there. But I thought that the compiler would compile different versions of the same function for different arguments anyway. Why the need for the difference

Vector{<:Real}

and

Vector{Real}

?

First, a bit of background. In Julia, there are no instances of abstract types. For example, typeof(1) == Int. Int is indeed a Real so 1 isa Real is true, however typeof(1) == Real is false. You can think of the type system as a directed acyclic graph in which the concrete types are the leaves.

If you have a Vector{Real}, this is a Vector the elements of which are Reals. However, they can be any concrete type which is descended from Real. So, for example you can have Real[1, 1.0], that is, a Vector with one element which is Int and another which is Float64. Because the container type is Vector{Real}, the compiler only knows that the elements are guaranteed to be Real, but it does not know what concrete type they are. They could be, Int, Float16, UInt8, who knows.

When you write Vector{<:Real} this is the union of all Vectors the elements of which are real, for example

julia> Vector{Real} <: Vector{<:Real}
true

julia> Vector{Int} <: Vector{<:Real}
true

julia> Vector{Float64} <: Vector{<:Real}
true

As should be becoming clear, it is “better” for performance reasons to have a Vector{Float64} than a Vector{Real} in which all of the elements happen to be Float64. This is because Vector{Float64} guarantees to the compiler that the elements are all Float64, whereas for Vector{Real} the elements can be any Real type.

So, back to the type signatures of functions. When you write Vector{Real} on an argument, you are requiring that the input be a Vector{Real} (perhaps confusingly, Vector{Real} is itself a concrete type, even though it has an abstract type as a parameter). In the vast majority of such instances, what you really want is to get a Vector the elements of which are Real. Yes, this could be Vector{Real}, but it can just as well be Vector{Int} or Vector{Float32} or Vector{BigFloat}. This is what Vector{<:Real} is telling it, the parameter of Vector must be a subtype of Real, but not necessarily equal to Real.

2 Likes

@ExpandingMan Thanks for this quick and detailed answer! Concluding for that, I would use the case

f(v::Vector{<:Real})

most often and only

f(v::Vector{Float64})

or

f(v::Vector{Int})

if I really need to do something fancy - e.g. bit-shifting would come to my mind or if I operate near memory bounds and need to restrict on how much I can store-, which crucially depends on how the numbers are stored in memory. I can not come up with a use case for

f(v::Vector{Real})

though. For what is it good then? Am I still getting something wrong?

1 Like

Indeed, I’ve been using the language extensively for almost 4 years and I have a hard time coming up with cases in which Vector{Real} is actually useful, especially as a type signature. In my experience, getting types correct can be a lot more difficult once IO is involved, so, for example, you might have a CSV parser which parses a file that has entries in a particular column like 1, 1.0, 2, 2.0 and istead of promoting to a float type (probably the better choice in this case) just takes it literally. This example is a bit contrived however, the “standard” Julia CSV parser, CSV.jl, is by now getting fairly mature, and is much more sophisticated than this, so this wasn’t a realistic example. It is also conceivable that a Vector{Real} can get produced somewhere by a “bug” (e.g. a package developer had to do something fancy to infer types, perhaps because IO is involved, and this failed in a circumstance the developer did not foresee).

You can safely assume that encountering a Vector{Real} would be very unusual.

It’s worth pointing out that f(v::Vector{<:Real}) does accept a Vector{Real} as an argument.

One other thing to add: most of the time you probably will want to go a step further and write AbstractVector{<:Real}. Julia and its common packages have a dizzying array of array types (pun intended) for all sorts of special purposes. One of the great strengths of Julia is that these tend to work together quite beautifully, precisely because Julia makes it easy to write some incredibly generic code, but this virtue is still dependent on developers not over-specializing their type signatures. Vector would basically assume that you are relying on a particular memory layout.

1 Like

@ExpandingMan Thanks for pointing the IO example out. I will keep in mind that my parsed numbers will have the the correct type. So far I am writing code in Julia purely for my own scientific use - but this (hopefully) will change soon, as more people make the switch from MATLAB/Python to Julia for the computational part of their projects.

Thanks for pointing the AbstractVector{<:Real} possibility out! I will use that. I assume for the same reason one should use AbstractArray as well?

2 Likes

AbstractArray is a little different. AbstractArray is an array of arbitrary rank, where AbstractVector == AbstractArray{T,1} where {T} is necessarily rank 1. So it depends on your use case. If you expect your argument to be just an arbitrary iterable, you might even consider not even having a type signature for it at all, since you could just as well get a Tuple, but in those cases as often as not AbstractArray does just fine. If you are expecting a rank-1 object for whatever reason (maybe you are taking an inner product or whatever), you shouldn’t hesitate to use AbstractVector.

The rule of thumb is usually to choose the most general type for which the logic in your function makes sense (even if this involves omitting the type signature altogether).

1 Like

@ExpandingMan Ok, I got this! Thank you very much! Your posts helped me a lot in understanding which types to use for declaring arguments!

1 Like

It’s worth noting that there’s no magic involved here: the complete definition of AbstractVector is:

const AbstractVector{T} = AbstractArray{T,1}

(see https://github.com/JuliaLang/julia/blob/042504df01e1ecdaf650390aabd82989907bde0c/base/array.jl#L23 )

1 Like

At the same time, it is probably not a useful restriction for dispatch, since it will not accept eg Any[1.0, 2.0, 3.0], which is a perfectly valid vector of Float64, and in a well-designed API any method that works for [1.0, 2.0, 3.0] should work for that, too.

1 Like