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
.
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:
convert
and promote_rule
, qualify them to extend insteadFoo
struct subtypes Number
to opt into arithmetic promotionconvert
method is for Foo{T}
instead of just Foo
convert
method to disambiguate Foo{T}(::Foo{T})
promote_rule
method is for Foo{S}
and T
not just Foo
and Number
promote_rule
method calls promote_type
to compute the type parameterAgain, 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.