Infer return type from ambiguous input

What is the best way to help Julia’s compiler infer that the output type of the following function can only be Union{Int64, Nothing}?

function inferme(x::Dict{String, Any}, y::String)
    if haskey(x, y)
        return Int(x[y])
    else
        return nothing
    end
end

Currently Base.return_types(inferme, (Dict{String, Any}, String)) = Any, which make sense (I think?) because there is no guarantee that x[y] yields something that doesn’t break Int(...). But how can I convince the compiler that it should just trust me? (In reality x is read from a JSON file, and its quite heterogenous, but the keys y that I will feed into this function will have integers)

Context: I am attempting to implement an interface where the required functions have their output types also specified (and the interface tests against this). I already know about RequiredInterfaces.jl :slight_smile:.

julia> function inferme(x::Dict{String, Any}, y::String)
           if haskey(x, y)
               return Int(x[y])::Int
           else
               return nothing
           end
       end

Give the compiler the guarantee.

In Julia 1.8.5:

julia> Base.return_types(inferme, (Dict{String, Any}, String))
1-element Vector{Any}:
 Union{Nothing, Int64}
5 Likes

Totally right :slight_smile: Thank you very much!!

1 Like

Just for my own learning, is there a method for Int(x) that does not return an Int or throw? Why isn’t that call sufficient guarantee?

1 Like

I myself do not know. But sometimes a method is overloaded so much (i.e., have so many bodies) that inference gives up. So even if all of them return Int the compiler can end up not inferring it.

well Int(3.5) throws InexactError for example but I don’t think that is the point here. The problem is that not all methods of Int might return an Int for all inputs. In principle someone can overload Int to do something strange like Int(str::AbstractString)=str or so. Proving that all methods return the same type for all inputs is probably impossible. I think Julia’s inference just gives up on calls like f(Any) no matter what f is.

If we do instead:

function inferme(x::Dict{String, T}, y::String) where T
    if haskey(x, y)
        return Int(x[y])
    else
        return nothing
    end
end

Then we can check what inference works on:

julia> Base.return_types(inferme, (Dict{String, Int}, String))
1-element Vector{Any}:
 Union{Nothing, Int64}
julia> Base.return_types(inferme, (Dict{String, String}, String)) # branch will always throw
1-element Vector{Any}:
 Nothing
julia> Base.return_types(inferme, (Dict{String, Float64}, String)) # branch sometimes throws sometimes works
1-element Vector{Any}:
 Union{Nothing, Int64}
julia> Base.return_types(inferme, (Dict{String, Real}, String)) # abstract type - give up
1-element Vector{Any}:
 Any

You can check this yourself, actually (Base.return_types is not, however, part of the public interface):

julia> collect(zip(map((f -> f(Int, Tuple{Any})), (Base.return_types, methods))...))
14-element Vector{Tuple{Any, Method}}:
 (Int64, Int64(x::Float64) @ Base float.jl:959)
 (Int64, Int64(x::Float32) @ Base float.jl:959)
 (Int64, Int64(x::Float16) @ Base float.jl:959)
 (Int64, Int64(x::Ptr) @ Core boot.jl:795)
 (Int64, Int64(x::Union{Bool, Int32, Int64, UInt16, UInt32, UInt64, UInt8, Int128, Int16, Int8, UInt128}) @ Core boot.jl:785)
 (Int64, (dt::Type{<:Integer})(ip::Sockets.IPAddr) @ Sockets ~/tmp/jl/jl/julia-d38348b476/share/julia/stdlib/v1.11/Sockets/src/IPAddr.jl:11)
 (Int64, (::Type{T})(x::BigFloat) where T<:Integer @ Base.MPFR mpfr.jl:403)
 (Int64, (::Type{T})(x::Enum{T2}) where {T<:Integer, T2<:Integer} @ Base.Enums Enums.jl:19)
 (Int64, (::Type{T})(x::Rational) where T<:Integer @ Base rational.jl:127)
 (Int64, (::Type{T})(z::Complex) where T<:Real @ Base complex.jl:44)
 (Int64, (::Type{T})(x::Base.TwicePrecision) where T<:Number @ Base twiceprecision.jl:265)
 (Int64, (::Type{T})(x::T) where T<:Number @ Core boot.jl:793)
 (Int64, (::Type{T})(x::BigInt) where T<:Union{Int128, Int16, Int32, Int64, Int8} @ Base.GMP gmp.jl:378)
 (Int64, (::Type{T})(x::AbstractChar) where T<:Union{Int32, Int64} @ Base char.jl:51)

The above is with a fresh REPL session, loading some packages could add new methods. StaticArrays, for example, used to have an Int constructor that returned non-Int values: make conversion of `Length` to `Int` safer by nsajko · Pull Request #1175 · JuliaArrays/StaticArrays.jl · GitHub

Relevant Julia issue: Require constructors and `convert` to return objects of stated type? · Issue #42372 · JuliaLang/julia · GitHub

1 Like