I am very excited about AbstractIrrational and am hoping to understand the intended guidelines for implementing subtypes of it. The abstract type itself was introduced by @stevengj in #24245, e.g. for representing exact multiples of π. @Jutho also mentioned a use case for square roots of rational numbers.
One concern/confusion I have is that in the both cases, not all possible values are actually irrational. For instance, 0π is rational, as is √4. Are types that derive from AbstractIrrational supposed to error when such values are given?
In my (tentative) opinion, it will be much more elegant for these types to allow any multiple of π, including 0π, as well as the square root of any rational number, including perfect squares. However, this suggests that a value represented by AbstractIrrational need not actually be irrational, and thus some of the overloads introduced in #24245 are not true in general: for instance, it is claimed that ==(x::AbstractIrrational, y::Real) = false because "Irrationals, by definition, can't have a finite representation equal them exactly." Other comparison functions, as well as iszero and isone, have similar issues in general.
Assuming it is agreed that even rational values can be represented by an AbstractIrrational, one path forward is to narrow the definition of some of the above methods to be only for Irrational rather than AbstractIrrational. And in any case, some documentation/guidelines on what methods somebody should overload for a new AbstractIrrational type would be very helpful.
On the one hand, it seems like unnecessary purism to restrict AbstractIrrational to irrational numbers when looking at a family of values like square roots or multiples of π. There's no technical requirement on the values except that they must have methods defined to convert them to floating-point types.
However, I think that the default methods like == should stand as-is for AbstractIrrational, since the main purpose of this type is to represent irrational values. Subtypes that extend it to exactly representable rational values can always overload ==.
On the other hand, it is also perfectly reasonable to give an error for a √4 "irrational" value. The main purpose of irrational types is to enter irrational constants, and there is no reason anyone should ever need this for √4, since they can always use the exact literal 2.
My understanding is that most irrational types should define:
==(::MyIrrational, ::MyIrrational)
hash(x::MyIrrational, h::UInt)
convert(::Type{F}, x::MyIrrational) # for at least F in {BigFloat,Float32,Float64}
and probably also show(io::IO, x::MyIrrational) for pretty-printing.
If MyIrrational can also represent rational values (which I wouldn't recommend in general, but it may be useful in special cases), then it should also implement isinteger, iszero, isone, == with Real.
Thanks, that's a useful definition of the interface. My use case is returning exact Clebsch Gordan coefficients, which is the square root of a rational number. I thought AbstractIrrational was a good starting point to inherit from, but if you think it is better that only truly irrational numbers should be a subtype, then that is fine with me. But it seems that what I need then amounts to defining a whole new Number type which mimics (=copies) a lot of the AbstractIrrational functionality.
No, in that use-case I think AbstractIrrational makes sense, since you need to return a single type in all cases for type-stability, but in that case you may need to overload four more methods.
If
MyIrrationalcan also represent rational values (which I wouldn't recommend in general, but it may be useful in special cases)
I think it's best to focus concretely on the intended use cases. So far, those mentioned mentioned include rational multiples or roots of an Irrational. I can think of other logical extensions, such as linear combinations of these as well. In each one of these cases, it is possible for the quantity to represent a value that is actually rational. To me, these actually seem like the most common use cases; in fact, I can't think of any reason to use AbstractIrrational (but not Irrational itself) for a type that will always be irrational. Can you think of one?
Because I believe types that can also represent rational values will be the most common use case for AbstractIrrational, I don't think it makes sense to say that this is not recommended in general.
It seems we agree that it would be undesirable for AbstractIrrational to be forced to error instead of representing a rational value. So I will focus here on implications of allowing this.
My main concern can be summarized as follows: I believe many of the methods should not have been generalized to AbstractIrrational in #24245.
< and <= for AbstractIrrationals, but this is not true in general.isinteger, isone, and iszero are not correct in general for such types.==(::AbstractIrrational, ::AbstractIrrational) = false is not true in general, and there is no good way of preventing this fallback from being called. For instance, if one package implements square roots of rationals while another implements multiples of pi, both types can represent the value 0, but the authors of these types will have no way of providing the correct behavior when one is compared to the other, without the packages knowing about the other type. Comparisons of such types will get hairy very quickly when there is a fallback method that can potentially be wrong. One thing this suggests is that all such irrationals should perhaps be in the same package, but even better IMO would be to eliminate these fallback methods that are not generally true, and allow them to be implemented explicitly (else MethodError) rather than with a fallback method that can be wrong. Yes, it will mean that those implementing such types will need to do a bit more work, but I expect this will happen rather infrequently, and I think correctness is worth it.Ping @stevengj (any thoughts on my post above?)
I think that if you subtype AbstractIrrational to implement a value that is not irrational, it is not unreasonable that you may have to provide specialized versions of a few more methods like isinteger. For ==, a conservative fallback of false seems much more useful to me than a MethodError — false is the default return value of == anyway for objects of different types.
I think that if you subtype
AbstractIrrationalto implement a value that is not irrational, it is not unreasonable that you may have to provide specialized versions of a few more methods likeisinteger.
On this we agree. However, the some of the fallback methods are incorrect for all potential use cases mentioned to date (other than for Irrational itself), and thus they will need to be specialized in all cases considered so far. Why, then, should these fallback implementations exist?
For
==, a conservative fallback offalseseems much more useful to me than a MethodError —falseis the default return value of==anyway for objects of different types.
Very good point.
Could we make a new type called SymbolicNumber or something similar to replace AbstractIrrational? There is no reason to restrict symbolic computation to irrational numbers. One clear use case would be something like SymbolicInt which would represent an integer symbolically allowing the compiler to do more complex optimizations.
I would prefer keeping the interface here as small as possible and defer more elaborate symbolic manipulations to packages.
The reason to want a SymbolicNumber object is that it is what anything of the others should be subtypes of which can not happen if it is not in base.
I agree with @garrison that I would think that most useful subtypes of AbstractIrrational could include numbers that are not irrational. Irrational{:...} is special since it just represents a number of isolated constants. But for any type that represents a complete family of numbers, there will likely be subsets which are simpler, i.e. just like Rational can represent integers and Complex can represent real numbers. Suppose there would be an AbstractComplex of which both Base's Complex and some PolarRepresentation would be subtypes, then, just to make a comparison, I don't think a ==(::AbstractComplex, ::Real) = false fallback would be very useful.
That's just my 2 cents, I don't actually have a strong opinion as long as it is clear what needs to be implemented.
I would like to resay my point that if we want to allow these numbers to sure rationals, we really should not have the type be named AbstractIrational. We can still fix this non breaking fassion by making AbstractIrational a subtype of a new symbolic type. Having 2 be a valid irrational, seems like a bad idea
Most helpful comment
I would like to resay my point that if we want to allow these numbers to sure rationals, we really should not have the type be named AbstractIrational. We can still fix this non breaking fassion by making AbstractIrational a subtype of a new symbolic type. Having 2 be a valid irrational, seems like a bad idea