typesjulia

promote_rule and type conversion for custom types in Julia


I am confused about how one can get automatic type conversion in Julia. This goes especially for function arguments.

Here is dummy code behaving in a way that I wouldn't expect:

import Base: convert, promote, +

struct Foo{T<:Number}
    data::T
end

convert(::Type{Foo}, x::Number) = Foo(x)
promote_rule(::Type{Foo},::Type{<:Number}) = Foo

+(p::Foo, q::Foo) = Foo(p.data + q.data)

Then in the REPL:

julia> q = Foo(3)
Foo{Int64}(3)

julia> 1 + q
ERROR: MethodError: no method matching +(::Int64, ::Foo{Int64})

What I would like it to return is

convert(Foo,1) + q

automatically without me having to type this. Is this possible?

A followup question is whether or not we can iterate this behavior assuming that we have an associative operation convert.


Solution

  • Getting conversion and promotion rules right is pretty tricky business. I designed the promotion system and I still have to do a fair bit of trial and error to get these things right—I did in this case too.

    The first issue with your code is pretty simple: you import promote but then you try to extend promote_rule; this results in your code defining its own function called promote_rule unrelated to Base.promote_rule. A good practice for avoiding this kind of mistake is to explicitly qualify functions when you extend them. This is syntactically awkward when extending operators, so for those I'll actually import them.

    The next issue is that promotion is only applied "automatically" to arithmetic operations on subtypes of Number. So in order to "opt into" arithmetic promotion fallbacks, Foo needs to subtype Number.

    The last issue is that Type{Foo} is not the singleton type of instances of Foo. What? Why? Because Foo is an abstract type. Observe:

    julia> Foo{Int} isa Type{Foo}
    false
    
    julia> Foo{Int} isa Type{Foo{Int}}
    true
    
    julia> Foo{Int} isa Type{<:Foo}
    true
    

    So you either need a type parameter in there or you need to mark the Foo parameter as covariant with <:Foo.

    Putting it all together, here is the correct version of the code:

    import Base: +
    
    struct Foo{T<:Number} <: Number
        data::T
    end
    
    Base.convert(::Type{Foo{T}}, x::Number) where {T<:Number} = Foo{T}(x)
    Base.convert(::Type{Foo{T}}, x::Foo) where {T<:Number} = Foo{T}(x.data)
    
    Base.promote_rule(::Type{Foo{S}}, ::Type{T}) where {S<:Number,T<:Number} =
        Foo{promote_type(S,T)}
    
    +(p::Foo, q::Foo) = Foo(p.data + q.data)
    

    In this version we have:

    Again, this is a pretty advanced topic so don't be dismayed if you find it tricky. It's also totally fine to encounter an error, fix it and repeat until your code works. Even then you may find an error later on. Eventually you'll hit all the corner cases and get it right.