Surprising behaviour for arguments of different types


#1

Suppose I want a function which takes varargs of any type, even different among arguments. Of course, there are two obvious ways to implement this:

f(x...) = typeof(x)
f(x::Vararg{Any}) = typeof(x)

Now I need to get the (most specific) type all arguments belong to, e.g. for (5, 6) I expect Int, for (5, 6.0)Real, for (5, nothing)Union{Int, Nothing} and so on. Before actually trying I was sure that replacing Any with T in the above example should do this, but

f(x::Vararg{T}) where {T} = typeof(x)

fails when its arguments do not have exactly the same type! E.g. f(5, 6.0) and f(5, nothing) say that no method matching. However, obviously, this method matches as there exists such T that both 5 and 6.0 are its instances.

The above happens for non-varargs as well.

So is there any way to work it around, i.e. get the most specific type all arguments belong to?


#2

When you specify a type parameter T across multiple arguments (implied by Varargs{T}), T must be concrete and it will need to be the same concrete type. This is described here in the manual.

I don’t think your desire for a “most specific” type is well defined. For example Union{A,B} could be considered the most specific for any two arbitrary types A and B. Yet, you want Real for (5, 6.0) instead of Union{Int,Float64}. Note that Union{Int,Float64} <: Real.

That said, there may be a way to get what you want: I’m just not personally aware of what it is, and I think you will have to be more specific about what precisely you require. Why exactly do you need this most specific type? There is very little advantage to an abstract type like Real over just using Any.


#3

Ok, I see - didn’t notice that a parametrized type has to be concrete. Well, I didn’t have any particular example in mind where this could be used, it was a general question. But one possible case is when one wants two methods, one being called when there is at least one nothing among the arguments. Like f(x::Vararg{T}) where {Nothing <: T} = nothing — which doesn’t work of course.


#4

AFAIK I don’t think there’s an easy way to do that with parametric types.

In that particular example, I would personally just check for x[i] isa Nothing inside the method body. Because julia compiles a different version of the method for each set of concrete types, the if will be compiled away, and you’ll get an efficient implementation specific to types of the input.


#5

Maybe you want f(x::Vararg{<:Any}) = typeof(x) … read about invariance vs. covariance in the manual. However, Vararg{Any} also matches any tuple of types; maybe Vararg and Tuple types are a bit special…?

Now I need to get the (most specific) type all arguments belong to

Perhaps you want f(x...) = typejoin(map(typeof, x)...).


#6

I don’t see any difference between how these two variants (with Any and <:Any) perform.

And your second suggestion f(x...) = typejoin(map(typeof, x)...) gives f(5, 6.0, nothing) == Any. Btw, it seems completely equivalent to f(x...) = eltype(x).