Wrap and inherit Number

I’ve seen topics touching this in different constellations, but:

How do people create a new type that would magically behave just like any subtype of (say) Number? So for example, if:

struct A{T <: Number}
    x::T
end

then I’d just be able to *, +, sin, exp, or sqrt instances of A and on top of that I’d still be able to (re)define additional specialized methods like say sqrt(a::A) = (a.x)^(1/3)?

Thanks and sorry for the overlap/repeat.

2 Likes

You need to define the operations for your new type.

The manual has a case study on Rational.

Dual in ForwardDiff is a very elaborate example.

isn’t there some macro that does that for me…? Or do you all use meta-programming to define all the possible methods?

But even then I can’t just go and

julia> a = OurRational(1,2)
OurRational{Int64}(1, 2)

julia> a*a
ERROR: * not defined for OurRational{Int64}

How would you expect Julia to know what you mean by * for a new type?

No no, I understand. But I had hoped that if I wanted to mimic, say, Float64, then there would have been an easy way to do that. Kind of like, in stead of starting from scratch: “here is a new type, you don’t know anything about it, let me tell you all you need to know about it”, starting from the top and “adjusting down”: “here is a new type, it’s exactly like Float64, except in this and this and that case”…

2 Likes

You could do something along the lines of

module PseudoNumbers

export PseudoNumber, getnumber  # this is the interface

abstract type PseudoNumber end

function getnumber end

import Base: +                  # and the rest ...

# should define for univariate and multivariate operators using macros,
# this is just an example
+(a::T, b::T) where {T <: PseudoNumber} = T(getnumber(a) + getnumber(b))

end

and then all the code you would need to write is

using PseudoNumbers

struct MyFloat{T} <: PseudoNumber
    x::T
end

PseudoNumbers.getnumber(mf::MyFloat) = mf.x

MyFloat(1.0) + MyFloat(2.0) # works out of the box

but I expect you would have to think about corner cases (lifting reals, how to combine two different types, etc).

1 Like

One option that we discussed a while back (https://github.com/JuliaLang/julia/pull/3292) is for a package to define a @delegate macro, so that you could do e.g.:

struct A{T<:Number}
    x::T
end
@delegate A.x Number

and it would use methodswith(Number, Base) to find all of the (exported) ::Number methods and define corresponding a::A methods that pass through a.x.

I don’t think anyone ever actually tried implementing such a thing, but it would be an interesting experiment.

(TypedDelegation.jl might be a reasonable place to work on this.)

7 Likes

I think that would be useful. Before you suggest I do that, I’ll just add: I suck at meta programming…

1 Like

While you can implement all the methods that a normal number has for your custom type, there is one thing where it gets tricky: do you want to inherit from Number or not? If you don’t, you can’t pass instances of your type to functions that only accept Numbers, even if your type behaves like a number. I don’t think there is a good solution to this right now, we’ll probably have to wait for either traits or interfaces in some future julia version for an elegant solution.

automatic promotion?

Automatic promotion should be avoided for custom types. They are good only for built-in types.

A few weeks ago @Per shared the intention to work on a library that may suit your needs, if your type will be a subtype of AbstractFloat (but it may not be the case):

1 Like

Ref this issue https://github.com/JuliaLang/julia/issues/9821.

Sounds great. I’m working on the tests now, but this whole thing is just for my idea with the Angle type. I defined all the functions needed for the conversions and the trigonometry, but now “all” I need is for this Angle type to behave like a Number (or AbstractFloat) in every other situation…

That’s totally wrong. The whole point of Julia’s promotion mechanism is that it is not just for “built-in” types, and is equally extensible to user-defined types. https://docs.julialang.org/en/latest/manual/conversion-and-promotion/

The real problem is that promotion rules are defined for mixed operations on different types, e.g. if you do a + b where a and b are different Number types then it will call promote(a,b) by default. This doesn’t help you at all for exp(x), for example, or for non-Number types, where the promotion code is not called in the default methods.

5 Likes

The @forward macro from Lazy.jl makes this a bit easier by allowing you to delegate a given list of functions to a specific field of a type: https://github.com/MikeInnes/Lazy.jl/blob/master/src/macros.jl#L267

(this is not nearly as fancy as the hypothetical @delegate, but at least it exists right now)

DataStructures.jl has an implementation of @delegate. If there’s interest, that could be turned into a (1 or 2 macro) package.

Cheers,
Kevin

3 Likes

To be clear, that implementation of delegate is not as full featured as @stevengj’s described version above.