Wrap and inherit Number

question

#1

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.


Creating custom DataFrame types
#2

You need to define the operations for your new type.


#3

The manual has a case study on Rational.

Dual in ForwardDiff is a very elaborate example.


#4

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


#5

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}

#6

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


#7

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”…


#8

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


#9

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


#10

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


#11

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.


#12

automatic promotion?


#13

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


#14

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


#15

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


#16

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…


#17

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.


#18

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)


#19

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

Cheers,
Kevin


#20

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