Why would closure break type inference?

function my_mean(A::AbstractArray)
  isempty(A) && return sum(A)/0
    n = length(A)
    x1 = first(A) / n
    _prom(x::T, y::S) where {T,S} = begin
      R = promote_type(T, S)
      return convert(R, x)
    end
    return sum(x->_prom(x,x1), A) / n
end

function my_mean_bad(A::AbstractArray)
    isempty(A) && return sum(A)/0
    n = length(A)
    x1 = first(A) / n
    let T = typeof(first(A)/n)
        return sum(x -> convert(promote_type(T, typeof(x)), x), A) / n 
    end
end

julia> @inferred(my_mean(Float32[]))
NaN32

julia> @inferred(my_mean_bad(Float32[]))
ERROR: return type Float32 does not match inferred return type Any
Stacktrace:
 [1] error(::String) at ./error.jl:33
 [2] top-level scope at REPL[5]:1

1 Like

The problem is not with the closure, but in captured variable T. If you replace T with typeof(first(A)/n) then all be ok. I’m not a big specialist in Julia compiler, but I think that if T is a dynamic DataType then the compiler can’t be sure that it has a constant value in all calls of promote_type, so at compilation time the result type of promote_type(T,…) is Any and for convert its Any too

Probably more accurately it sounds like this: at compilation time of the closure we only know the type of T, that is DataType but not its value. So we cannot recognize the result of promote_type and the result type of convert at compilation time

1 Like

This is missing a terminating end. I think that you can just rewrite this as

function my_mean_bad(A::AbstractArray)
    isempty(A) && return sum(A)/0
    n = length(A)
    x1 = first(A) / n
    sum(x -> first(promote(x, x1)), A) / n
end

which infers fine.

That version does not work if an array has “missing” values.

Your original question does not mention missing at all though.

The other function does not infer either for missing, eg

julia> @inferred(my_mean([missing 1f0]))
ERROR: return type Missing does not match inferred return type Any

What would you want to do for missing values, ignore them (see skipmissing), or return missing as the mean?

1 Like

Yes, the my_mean() version does not infer with the missing values as well, but it executes without an error, unlike the one you propose.

Edit: my_mean() is similar to Base.sum() on “missing”

julia> @inferred(Base.sum([1,missing]))
ERROR: return type Missing does not match inferred return type Union{Missing, Int64}
function my_mean_bad(A::AbstractArray)
    isempty(A) && return sum(A)/0
    n = length(A)
    return sum(x -> convert(promote_type(typeof(first(A)/n), typeof(x)), x), A) / n 
end

infers fine and executes without an error with the missing values

The question isn’t how to fix the function, but why the type inference fails on the seemingly equivalent program.

Fixed, thanks

This version also breaks the inference

function my_mean_bad(A::AbstractArray)
   isempty(A) && return sum(A)/0
   n = length(A)
   x1 = first(A) / n
   T = typeof(first(A)/n)
   return sum(x -> convert(promote_type(T, typeof(x)), x), A) / n
end

I think you need to specialize on the type. This dummy function barrier works:

function my_mean_bad(A::AbstractArray)
    isempty(A) && return sum(A)/0
    n = length(A)
    x1 = first(A) / n
    _core(::Type{T}) where {T} = sum(x -> convert(promote_type(T, typeof(x)), x), A) / n
    _core(typeof(x1))
end

Similarly _core(x1::T) where T etc.

Yes, that’s another version that works. Probably, a manifestation of this issue