Understanding how to implement numbers in Julia

To understand how to implement a number in Julia, I tried to implement my own complex number type:

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

MyComplex(re::Real) = MyComplex(re,zero(re))
Base.conj(z::MyComplex) = MyComplex(z.real,-z.imag)
real(z::MyComplex) = z.real
imag(z::MyComplex) = z.imag

then I define basic arithmetic

function Base.:+(a::MyComplex,b::MyComplex)
    MyComplex(a.real + b.real, a.imag + b.imag)
end

function Base.:-(a::MyComplex,b::MyComplex)
    MyComplex(a.real - b.real, a.imag - b.imag)    
end

function Base.:*(a::MyComplex,b::MyComplex)
    MyComplex(a.real * b.real - a.imag * b.imag, a.real * b.imag + b.real * b.imag)
end

function Base.:/(a::MyComplex,b::Real)
    return MyComplex(a.real/b,a.imag/b)
end 

function Base.:/(a::MyComplex,b::MyComplex)
    num = a * conj(b)
    den = real(b * conj(b))
    return num/den
end 

What do I do from here so that my new MyComplex number type just works with math functions such as sin cos exp sqrt? I thought if I implement the basic arithmetic it will just work with all of the built in math functions.

Then, some of the operations fails:

julia> 2/MyComplex(0,2)
ERROR: promotion of types Int64 and MyComplex{Int64} failed to change any arguments
Stacktrace:
 [1] error(::String, ::String, ::String) at ./error.jl:42
 [2] sametype_error(::Tuple{Int64,MyComplex{Int64}}) at ./promotion.jl:306
 [3] not_sametype(::Tuple{Int64,MyComplex{Int64}}, ::Tuple{Int64,MyComplex{Int64}}) at ./promotion.jl:300
 [4] promote at ./promotion.jl:283 [inlined]
 [5] /(::Int64, ::MyComplex{Int64}) at ./promotion.jl:314
 [6] top-level scope at REPL[73]:1

as the error suggests, you need to implement the promotion rules:

because you sub-typed Number, as soon as you have promotion rules in place, these will start working. (because + - * / has fallback methods which perform promotions first)

5 Likes

I don’t think the interface for numbers is formally and extensively documented (yet?), but there are a couple of threads here on discourse where this topic was discussed. You might find valuable information there:

From what I see and off the top of my head, I’d say that apart from promotion rules, you’ll still be missing:

  • a unary minus operator
  • comparison operators
  • probably a zero method
5 Likes

How do I implement promotion rules? I added this in my script

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

MyComplex(re::Real) = MyComplex(re,zero(re))
Base.conj(z::MyComplex) = MyComplex(z.real,-z.imag)
real(z::MyComplex) = z.real
imag(z::MyComplex) = z.imag

promote_rule(::Type{MyComplex{T}}, ::Type{S}) where {T<:Real,S<:Real} = MyComplex{promote_type(T,S)}
promote_rule(::Type{MyComplex{T}}, ::Type{MyComplex{S}}) where {T<:Real,S<:Real} = MyComplex{promote_type(T,S)}

following the example given for Rational type: https://docs.julialang.org/en/v1/manual/conversion-and-promotion/#Case-Study:-Rational-Promotions and when I try to run promote(MyComplex(2,0),2) it errors out

julia> promote(MyComplex(2,0),2)
ERROR: promotion of types MyComplex{Int64} and Int64 failed to change any arguments
Stacktrace:
 [1] error(::String, ::String, ::String) at ./error.jl:42
 [2] sametype_error(::Tuple{MyComplex{Int64},Int64}) at ./promotion.jl:306
 [3] not_sametype(::Tuple{MyComplex{Int64},Int64}, ::Tuple{MyComplex{Int64},Int64}) at ./promotion.jl:300
 [4] promote(::MyComplex{Int64}, ::Int64) at ./promotion.jl:283
 [5] top-level scope at REPL[14]:1

EDIT: if I use Base.promote_rule instead of just promote_rule it works (ish, I need to implement some more functions), how do I know when to use Base. instead of the function directly?

whenever you want to add a new method for an existing function from another module, you need to either import it, or qualify it. So either

import Base.promote_rule
promote_rule(..) = ...

or Base.promote_rule(...)=... as you did. (Same for any other module, e.g. if you want to add a method to a function from some package PkgX, then you’d do the same).

Often the Base.promote_rule(...)=... way is preferred (e.g. it’s recommended in YASGuide which is the style guide we use at my work) because it makes it more obvious that you’re extending a function from another module (since the import statements may be in some other file so you might forget that you’ve imported the function).

If you don’t do this, you’re defining a new function instead of adding a method to an existing function.

3 Likes