How to enable type inference on own types similar to what julia supports for nothing and missing?

I try to understand when to use Union types versus constructing custom type hierarchies.

One peculiarity is how nothing / missing behave like you would hope for

julia> f(x) = isodd(x) ? nothing : Some(x)
julia> f.([1,2,3])
3-element Array{Union{Nothing, Some{Int64}},1}:
 nothing
 Some(2)
 nothing

as you can see, this function returns an Array of lovely concrete type. The same works for missing by the way.

However if I define my own type, lets say a singleton type like Nothing, type inference is not able to get it, but infers Any instead of the lovely Union seen above.

julia> struct MySingleton end
julia> g(x) = isodd(x) ? MySingleton() : Some(x)
julia> g.([1,2,3])
3-element Array{Any,1}:
 MySingleton()
 Some(2)
 MySingleton()

I hope that this is not hardcoded somewhere in the typeinference, but that I only need to overwrite some helper functionalities within Base to make the inference work properly. Does someone know which things to overload?

I tried to overload Base.promote_rule, however it does not change the above results at all. Hopefully there is some other hidden gem.

3 Likes

Julia does not have any (or at least many) hard-coded features. In this case it’s the promotion machinery at work:

julia> promote_type(Nothing, Int64)
Union{Nothing, Int64}

So you just (?) need to define promote_type for your type.

thanks a lot for your reply!
unfortunately that just does not work.

On top, the julia documentation Conversion and Promotion · The Julia Language recommends to overload promote_rule, and as said, that does not work either…


By now I tried to mimic all of promote_rule stuff from Nothing

nonmysingleton(::Type{T}) where {T} = Core.Compiler.typesubtract(T, MySingleton)

Base.promote_rule(T::Type{MySingleton}, S::Type) = Union{S, MySingleton}
# CAUTION: this leads to method ambiguities
function Base.promote_rule(T::Type{>:MySingleton}, S::Type)
    R = nonmysingleton(T)
    Core.println("R = $R")
    R >: T && return Any
    T = R
    R = promote_type(T, S)
    return Union{R, MySingleton}
end

Base.promote_type(T::Type{MySingleton}, S::Type) = Union{S, MySingleton}
Base.convert(::Type{T}, x::T) where {T>:MySingleton} = x
Base.convert(::Type{T}, x) where {T>:MySingleton} = convert(nonmysingleton(T), x)

# EDIT: together with the following two lines it works
Base.promote_typejoin(::Type{MySingleton}, T::Type) = Union{T, MySingleton}
Base.promote_typejoin(T::Type, ::Type{MySingleton}) = Union{T, MySingleton}

but still does not work…
(the Core.println actually does not print anything, hence does not get called)

1 Like

It looks like Base.promote_typejoin has some special behavior for Nothing and Missing. You should be able to implement the same thing for your type by following that example, although I’m not entirely sure that’s the right thing to do.

Whoops, forgot to post the relevant link: julia/promotion.jl at db9063138bbcf8d35ff2e4e55486506602615125 · JuliaLang/julia · GitHub

4 Likes

Thanks a lot, adding two Base.promote_typejoin rules solved it,

however now from time to time I get errors, that method resolution is ambiguous now. I think this is be design and denoted by the implementation of missing

as you can see, missing is indeed not only implementing promotion for missing, but also accounts for interactions with nothing. I.e. it seems if you want to add a new type like MySingleton you have not only to add the methods above, but you have to model the interactions with all other such types like nothing and missing from Base.

This together with Base._promote_type implementation actually looks like hardcoded to me… you can hack arround but rather complicated and non-intuitive… a bit unfortunate