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
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)
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:
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.