Unbound type parameters detected in Union{Missing,T}

Hi,
Aqua.jl complains about Unbound type parameters detected in the following MWE:

julia> module mymod
       
       function foo(x::AbstractArray{<:Union{T,Missing}}) where {T<:Real}
           m = .!ismissing.(x) .&& .!isnan.(x)
           return [ifelse.(m, x, T(0))]
       end
       
       function foo(x::AbstractArray{<:Missing})
           m = .!ismissing.(x) .&& .!isnan.(x)
           return [ifelse.(m, x, 0)]
       end
       end
Main.mymod

julia> Aqua.test_unbound_args(mymod)
Unbound type parameters detected:
[1] foo(x::AbstractArray{<:Union{Missing, T}}) where T<:Real @ Main.mymod REPL[32]:3
Test Failed at /Users/ferreol/.julia/packages/Aqua/EPo21/src/unbound_args.jl:37
  Expression: isempty(unbounds)
   Evaluated: isempty(Method[foo(x::AbstractArray{<:Union{Missing, T}}) where T<:Real @ Main.mymod REPL[32]:3])

ERROR: There was an error during testing

I don’t really understand why as I think I have covered all the cases:

julia> a = [1.0 missing 2.];

julia> b = ones(3);

julia> c = [missing];

julia> mymod.foo(a)
1-element Vector{Matrix{Float64}}:
 [1.0 0.0 2.0]

julia> mymod.foo(b)
1-element Vector{Vector{Float64}}:
 [1.0, 1.0, 1.0]

julia> mymod.foo(c)
1-element Vector{Vector{Int64}}:
 [0]

Any idea about why it is still unbounded?

The problem is that the test (which is part of the Test stdlib and not Aqua) will look at each method individually. So even though the unboundness case will never actually happen due to the second method being defined, it will still be reported.
This is tracked both in Aqua (as Unbound argument should not be counted if a method isn't called · Issue #86 · JuliaTesting/Aqua.jl · GitHub) and in julialang (as Test.has_unbound_vars should show some respect for history · Issue #28086 · JuliaLang/julia · GitHub).

1 Like

Thanks,
Is there a way to make it bounded in a single method?

I think it should be possible by defining three methods with signatures:

foo(x::AbstractArray{Union{T,Missing}}) where {T <: Real}
foo(x::AbstractArray{T}) where {T <: Real}
foo(x::AbstractArray{Missing})

It does not seems so:

julia> module newmod
       foo(x::AbstractArray{Union{T,Missing}}) where {T <: Real} = ifelse.(.!ismissing.(x) .&& .!isnan.(x), x, T(0))
       foo(x::AbstractArray{T}) where {T <: Real}  = ifelse.(.!isnan.(x), x, T(0))
       foo(x::AbstractArray{Missing}) = zeros(size(x))
       end

julia> Aqua.test_unbound_args(newmod)
Unbound type parameters detected:
[1] foo(x::AbstractArray{Union{Missing, T}}) where T<:Real @ Main.newmod REPL[20]:2
Test Failed at /Users/ferreol/.julia/packages/Aqua/EPo21/src/unbound_args.jl:37
  Expression: isempty(unbounds)
   Evaluated: isempty(Method[foo(x::AbstractArray{Union{Missing, T}}) where T<:Real @ Main.newmod REPL[20]:2])

ERROR: There was an error during testing

Anyway, I have my answer and I will probably deactivate the test with broken=true
Thanks again

That seems like a bad practice though. Do you really need to dispatch on things like this?

1 Like

Yep, besides just being piracy, I’ve always found dispatching on a X{<:Union{T, Missing}} where T quite fraught to use correctly… I’m not even certain how they are working with the rules of Julia’s type system to begin with!

Even more generally, simply dispatching on eltypes can be prone to surprises. It’s not hard to end up with an array whose elements are all some T but whose eltype is something wider.

1 Like

I was a bit provocative as I didn’t find a way to properly exclude the function (that is a constructor of a struct in my package).

Actually, I don’t really need those function as I’ve just added it to make my package more composable in the case I’ll register it.

I never use missing because it usually ruins the performance:

julia> N = 1_000
1000

julia> g(x) = sum(x->ifelse(ismissing(x), 0 ,x),x,dims=2)
g (generic function with 1 method)

julia> h(x) = sum(x->ifelse(isnan(x), 0 ,x),x,dims=2)
h (generic function with 1 method)

julia> @btime g($([ifelse(rand().<0.4,missing,randn()) for  _ in 1:N, y in 1:N ]));
  4.201 ms (3 allocations: 9.08 KiB)

julia> @btime h($([ifelse(rand().<0.4,NaN,randn()) for  _ in 1:N, y in 1:N ]))
  159.169 μs (3 allocations: 8.08 KiB)

I don’t really understand what do you mean by piracy in this case?

It’s sure that if T is not concrete then dispatching on X{<:Union{T, Missing}} is a bit useless but not faulty. Out of curiosity, is there a prefered mechanism to concretize an array (let say an Vector{Number} where every elements are in fact Float64), or to dispatch on concrete vs non-concrete T

Sorry, you’re right this isn’t piracy as you’re defining your own function.