Conversion and promotion

While working on an exercise on exercism, I found myself struggling with the issue of conversion and promotion.
The exercise called for implementing a user-defined type for complex numbers. Defining the type and the mathematical methods was straightforward, but I when I implemented the conversion and promotion rules, I either had a missing conversion or an ambiguity.
In one of the published solutions, the conversion/promotion is defined as follows:

struct ComplexNumber{T<:Real} <: Number 
    Re::T
    Im::T
end
const jm = ComplexNumber(0, 1)
# a bunch of math operations....
promote_rule(::Type{ComplexNumber{T}}, ::Type{ComplexNumber{S}}) where {T<:Number,S<:Number} = ComplexNumber{promote_type(T,S)}
convert(::Type{ComplexNumber{T}}, x::ComplexNumber{S}) where {T<:Number,S<:Number} = ComplexNumber(convert(T, x.Re), convert(T, x.Im))
ComplexNumber(x::Real, y::Real) = ComplexNumber(promote(x,y)...)

Please explain why this combination of promotion/conversion/construction methods is the (only/recommended?) way
What are the general guidelines when integrating a new type with existing types?

*my take on the struct

Where the components of a struct all share the same type, and there is more than one type that may be used when constructing the struct, then

struct StructName{T<:supertype_for_components}

Where the components of a struct all share the same type, and there is more than one type that may be used when constructing the struct, and the structured type is intended to behave (participate in the Julia ecosystem) as a member of a larger type, then

struct StructName{T<:supertype_for_components} <: supertype_for_behavior

note that supertypes must be abstract types

In Julia, isabstracttype(Complex) == false, so we cannot use Complex as the supertype for behavior. We use supertype(Complex) == Number. If there were no Complex type in Julia, knowing that the type of the components is some kind of Real, we would use supertype(Real) == Number. Either way

struct ComplexNumber{T<:Real} <: Number

It is best practice to name structs using titlecase, and to name the constituent fields (struct components) using lowercase. It is helpful to use fieldnames that are more self-explanatory than not.

struct ComplexNumber{T<:Real} <: Number
    real::T
    imag::T
end

Where all components are of the same type, and that type is parameterized, It is utilitarian to provide a constructor that does not require the explicit parameter.

struct ComplexNumber{T<:Real} <: Number
    real::T
    imag::T
end

ComplexNumber(real::T, imag::T) where {T<:Real} =
    ComplexNumber{T}(real, imag)

It makes sense to allow construction of a ComplexNumber from a Real value;
this allows that and preceding construction.

struct ComplexNumber{T<:Real} <: Number
    real::T
    imag::T
end

ComplexNumber(real::T, imag::T=zero(T)) where {T<:Real} =
    ComplexNumber{T}(real, imag)

and it is nice to show it well

const ImaginaryPostfix = "im"

function Base.show(io::IO, x::ComplexNumber)
    sgn = signbit(x.imag) ? " - " : " + "
    str = string(x.real, sgn, abs(x.imag), ImaginaryPostfix)
    print(io, str)
end
1 Like

*my take on the promotion

from the docs:

promote(xs…)
Convert all arguments to a common type …

promote_rule(type1, type2)
Specifies what type should be used by promote when given values of types type1 and type2. This function should not be called directly, but should have definitions added to it for new types …

promote_type(type1, type2, ...) exists:
Calling promote_type can be quite helpful.
Adding methods to it can create hard to find problems.
Add methods to promote_rule instead where possible.

For a new type (here, the struct ComplexNumber{T}), promote does not yet know how to convert two values each with its own, distinct, component type to a common ComplexNumber type.

However, Julia knows rules about how to promote two Real types
using promote_rule. With that, so does promoteknow how to promote two Real values of distinct types. promote_ruleis type-oriented andpromote` is value-oriented.

note the way to define promotion for a parameterized type

function Base.promote_rule(::Type{C1}, ::Type{C2}) where {T1, T2, C1<:ComplexNumber{T1}, C2<:ComplexNumber{C2}}
    T = promote_type(T1, T2)
    ComplexNumber{T}
end

(the case where T1 == T2 is handled automatically)

we can use this to define a constructor that convert the ComplexNumber’s parameterization

ComplexNumber{T}(x::ComplexNumber) where {T} =
    ComplexNumber(T(x.real), T(x.imag))

We define complex addition for two arguments of the same type (both types parameters’ are the same)

function Base.:(+)(x::ComplexNumber{T}, y::ComplexNumber{T}) where {T}
    real = x.real + y.real
    imag = x.imag + y.imag
    ComplexNumber(real, imag)
end

now, automagically, we support addition with mixed type ComplexNumbers

# using the prior post and this one

julia> a = ComplexNumber{Float32}(1.5, 0.75)
julia> b = ComplexNumber{Float64}(0.5, 1.25)
julia> a + b
2.0 + 2.0im
2 Likes