Making my own custom numeric type

I would like to make my custom Type behave like a numeric type. Ideally I would specify a custom convert() function that would do some smart computations to summarize the type’s fields as a single value. What is the cleanest and most design friendly way to do this? I got it working but it seems like some kind of inheritance method would be more reusable.

julia> struct Ratio
           num::Float64
       end

julia> using Statistics

julia> Ratio[Ratio(1), Ratio(2), Ratio(3)]
3-element Vector{Ratio}:
 Ratio(1.0)
 Ratio(2.0)
 Ratio(3.0)

julia> Statistics.mean(ans)
ERROR: MethodError: no method matching /(::Ratio, ::Int64)
[...]
julia> Statistics.mean(ratios::Vector{Ratio}) = Statistics.mean(getfield.(ratios, :num))

julia> Statistics.mean(ratios)
2.0

Ideally I don’t want to create my own duplicate method for every function in Statistics and other modules, but just some way that those functions can see my value as a numeric type natively.

You really need to inherit from Number or Real. But you will still need to define all the method like /.

If you want the full interface without so much work, you can use:

1 Like
struct Ratio
    num::Float64
end

operators = (:+, :*, :/, :-, :÷, :\, :^, :%)

for i in operators
    @eval Base.$(i)(a::Ratio, b::Ratio)  = Ratio(($i)(a.num, b.num))
    @eval Base.$(i)(a::Ratio, b::Number) = Ratio(($i)(a.num, b))
    @eval Base.$(i)(a::Number, b::Ratio) = Ratio(($i)(a, b.num))
end

fun = (:sin, :cos, :tan, :cot, :sec, :csc,
       :sinh, :cosh, :tanh, :coth, :sech, :csch, 
       :asin, :acos, :atan, :acot, :asec, :acsc,
       :asinh, :acosh, :atanh, :acoth, :asech, :acsch,
       :sinc, :cosc, :exp, :sqrt, :abs, :log)

for i in fun
    @eval Base.$(i)(a::Ratio) = Ratio(($i)(a.num))
end

sqrt(Ratio(3.0)) #works
sqrt(3.0)

using Statistics

mean([Ratio(1), Ratio(2), Ratio(3)]) # works

What do you think of this? is this a good approach for this kind of problem?

You are probably doing more work than necessary. Inheriting from Real is probably a good idea.

See Ratios.jl/Ratios.jl at master · timholy/Ratios.jl · GitHub

2 Likes

Thank you for thoughtful feedback.

  1. AbstractNumbers.jl - I tried this but ruled it out because it brings in a ton of dependencies that I will not be using.
  2. Overloading every operator - I did not try this because it wanted an inheritance approach.
  3. My preferred solution just came from copying from the Ratios.jl source code:
julia> struct NewRatio <: Real
       num::Float64
       end

julia> Base.convert(::Type{T}, r::NewRatio) where {T<:Real} = T(r.num)

julia> Statistics.mean([NewRatio(1), NewRatio(2), NewRatio(3)])
2.0
2 Likes

The problem with that approach is you will lose your object at any opration. Usually you will want to do things like keep your type through multiplication by a scalar or addition with itself. Otherwise it doesn’t have much of a use ase.

Great point. If your use case is to have a rich record with tons of fields - such as a record for each customer - and you want to have some numerical way to summarize it (like current year purchases), you probably want the record to be destroyed after any computation. Because once you add two customers together, you will have sum of their purchases but you wouldn’t want to have sum of their number of transactions or some sum of their names.

But for something like a stock price daily quote, you might still want - and you could average the high, the low, and so on - to keep the record instance alive.