Numeric types and their relationships



Suppose I need to define a parametric type with numeric values that are real numbers, but not integers.

Initially I thought of doing something like:

struct Foo{Integer<:T<:Real} end

but I am aware that it has two issues:

  1. the syntax Integer <: T means a type T that can at least hold an integer, so I am not excluding them
  2. Integer, AbstractFloat, Rational, and Irrational are siblings in the type tree, so asking Integer <: Rational returns false
julia> subtypes(Real)
4-element Array{Union{DataType, UnionAll},1}:

Can you explain the rationale for this design and comment on a possible solution for the original problem?




Thanks @yakir12, Union sure solves it. :+1:

I am still curious about why the type hierarchy was not chosen so that Integer <: Rational, probably a performance bottleneck?


Unchecked and maybe wrong, but isn’t Rational a particular concrete implementation of a rational number? IIRC it isn’t an abstract type. Please correct me if I’m wrong.


There are a few ways to look at it. One is that implementations of rationals are not a subset of implementations of integers. Another is to ask this question: how would you define rationals without already having defined the integers?

Another view is a bit more philosophical, but fundamentally similar… Although the integers are isomorphic to a subset of the rationals (those with unit denominator), and are conventionally identified with this subset, they are not the same as this subset. If they were, you’d have a circular definition of the integers since you have to define the rationals in terms of the integers. This is similar to the situation in a computer implementation: you define rationals in terms of integers, so the integers cannot be a subtype of rationals.


I get the implementation argument, but the mathematical one, I disagree. The rationals are defined in terms of the integers, and there is no circular argument:

The real line is constructed in this order N -> Z -> Q -> R (no cycles).


Yes, that’s exactly what I’m saying – the integers are defined before the rationals, so the integers cannot actually be part of the rationals, just identified with a subset of them.


I don’t understand your explanation, what you mean by part? Exactly because one can identify the integers with an equivalent subset of the rationals, I don’t see how the Julia Integer <: Rational operation should return false.

If there are practical implementation difficulties, then that is another history.


<: is the subtype operator, not the subset operator. Integer <: Rational returns false because Integer is not a subtype of Rational.

help?> <:
search: <:

  <:(T1, T2)

  Subtype operator, equivalent to issubtype(T1, T2).

  julia> Float64 <: AbstractFloat
  julia> Vector{Int} <: AbstractArray


Got it @John_Gibson, I was confused with the operation, makes sense now.


Actually, I am still confused, why is Integer not a subtype of Rational? Given an object of type Int, wouldn’t be ideal to say it is also a Rational with denominator 1?

I think I am seeing the cycle argument issue that @StefanKarpinski mentioned with defining one type in terms of the other…


I believe the Rational type is a Union type of the parametric type Rational{T} where T<:Integer, so it is not possible to make Integer a subtype of it.

Maybe, if there was a AbstractRationaltype, than this could be done, where Rational <: AbstractRationaland Integer <: AbstractRational. But I believe making Integer<:Rational is impossible.

Edit: I’m not testing these stuff I’m saying, just conjecturing.


Despite mathematical convenience, being “isomorphic to” is not the same as being “identical to”. Mathematically, the rational numbers are defined as the equivalence classes of pairs of integers (a,b) where b \neq 0 under the equivalence relation

\qquad (a, b) \equiv (c, d) \iff ad = bc

The rational numbers with representatives (a,1) form a subset of this set of classes that are isomorphic to the ring of integers, but they are not formally the same objects, they just behave the same. In symbols: a \neq \left[(a,1)\right]_\equiv. These cannot be the same object because of the axiom of regularity. (Or more simply, because a is a finite set whereas the equivalence class \left[(a,1)\right]_\equiv is an infinite set of pairs of integers, so they’re quite different objects.)

When mathematicians consider the integers to be a “subset” of the rationals, they are not talking about the same set of integers that were used in the above definition, they are talking about the subset of rationals that behave like those “original integers”. For humans, this distinction is easy to gloss over and we transparently switch between which meaning of “the integers” we’re talking about without even realizing it. But they are very much distinct mathematical entities.

In computers, we have to do a similar construction and for the same reason, the integers which the rationals are defined in terms of cannot logically be a subset of the rationals that they are used to define. If this doesn’t make sense to you, that’s fine (I’m not going to argue the point further), but my point is that even mathematically, the integers from which the rationals are constructed are not a subset of the rationals, they are merely isomorphic to a subset of the rationals. In Julia, we model this kind of isomorphism between structurally different objects by just giving them compatible behaviors, rather than with a type relation.


It makes total sense, I see the difference and its implications.


I wonder if using traits might be better, because, unlike subtyping, you can add other types and traits later.
It’s such a powerful thing in Julia, once you get the hang of it (I do wish some syntax for traits like we had discussed at JuliaCon 2016 had made it into the feature freeze for v0.7!).
Instead of a hierarchy, you’d use traits for whether a numeric type represented rational or irrational, signed integer, unsigned integer, binary float, or decimal float, fixed or arbitrary precision, etc.