Feature suggestion: Rational primitive type
The Rational type wraps a Record with two integer fields: Numerator and Denominator. It will always internally, accurately represent the rational number as it was calculated or expressed. As a result, it has no rounding errors unless it is cast to some other type. When needed for formatting in a string, it is converted explicitly or by default into decimal format with given amount of characters after the decimal point.
Why?
I believe that floats introduced a costly problem, like null. In my opinion most use cases of floats would be better off using rational numbers. In fact I declare my own struct with numerator and denominator fields, custom own arithmetic operations and only convert to a float(-like representation) if necessary for representation, or have some explicit declaration when you care more about performance and range/scale than is probably possible with Rationals.
Floats are not Real
Floats do not constitute the set of Real numbers. The only way I can think of to represent irrational numbers is algebraically, and/or by means of some infinite or recursive expression. Floats are really a specific case of rational numbers! Well, why restrict our rational numbers to have a denominator of strictly powers of two?
Benefits and drawbacks
The float type might be more performant in arithmetic and perhaps its range is greater with the same amount of bits, meaning it can represent smaller and larger numbers. While giving up the precision. Rationals have no intermediate rounding errors. That results in easy comparison with other rationals, no missed cases in conditionals, no snowball rounding errors trickling down, among other thinking overhead of resolution of the possible inaccuracy. Easier testing, greater confidence of the code, cleaner design.
Generalisation of Decimal and Float
The Decimal type seems to follow a similar thinking and this could be a more general case where the denominator is not limited to powers of ten. The Float might then also be a special case of Rational type instance, and be evaluated and represented as a float for performance at compile-time.
Upon its initialisation, it can resolve its common factors between the numerator and denominator. e.g. Rational{ num: 2, den: 4 } resolves in Rational{ num: 1, den: 2 }.
Hi all, as per Richard's request I've pasted my idea here. Hope it's clear.
Sounds interesting! One question I have is what happens if the numerator or denominator become too large to fit in the chosen int type? I imagine this could quickly happen if you add or multiply a bunch of rationals together that are relatively prime.
Good question! I think that that the problem is the same for simple int32/int64 types, apart from the fact that one might want to ‘reduce’ the rational after arithmetic such that the numerator and denominator remain coprime. Performance might be an issue there. Or do you think that may be problems I don’t foresee?
I agree that the numerator and denominator should be simplified after each operation since custom equality isn't needed then. But what I'm getting at is that something like 1/2 + 1/3 + 1/5 + 1/7 + 1/11 + ... + 1/prime_n will result in n/(2*3*5*7*11*...*prime_n) which eventually overflow the denominator and I'm wondering what the rational type will do then? I'm guessing it will have to round to the nearest representable value but in that case this statement
Rationals have no intermediate rounding errors. That results in easy comparison with other rationals, no missed cases in conditionals, no snowball rounding errors trickling down, among other thinking overhead of resolution of the possible inaccuracy.
wouldn't be true.
That said, I've never used rationals before so I don't know if this is an issue in practice.
Okay got it. How is that solved with multiplying 235711*… with regular int types? Some overflow error or cast to a separate type without a fixed number of bits?
Normal Int math also has overflow issues but in that case I don't think there's any surprise. Everyone understands that if you make the int too big, it will overflow.
But with rationals, you can just be doing math with reasonable looking numbers and suddenly the denominator overflows because those numbers couldn't simplify enough.
This also seems like a problem if you're using trig functions. Something like Rational.cos 1/2 will return a value that (if you're trying to be as accurate as possible) will have a very large numerator and denominator that can easily overflow if you add/multiply it with something else.
I want to stress, I haven't used Rationals before so maybe these are non-issues or there's some solution I don't know of (I'm not trying to nitpick I promise :sweat_smile: )
Yes, valid point. Perhaps certain operations, like cosine, return a number that is rounded for all practical purposes, except if it's still in a mathematical expression. In the end, it's either used for further operation, printed, or stored. I'm not sure whether it then makes sense to allow the cosine to return a rational, unless it's obvious it's rounded.
I mean, the number actually doesn’t belong in the rational set, but in the real set (with some exceptions like cos(pie). So if the function mathematically has a range that includes irrationals, you you’ll never get complete accuracy. Unless you represent it algebraically.
And, multiplicative arithmetic could be as fast or faster, but addition not so much, is my intuition.
Your "why's" for rational numbers seem to be mostly against floating point types and how they break reasoning due to having NaN and such.
Since our decimal type doesn't have NaN and infinity and such, it should be much easier to reason about. Since it is also in base 10, whatever is printed out will always exactly match what is stored. With these in mind, what do you think would be the main use cases where rational types will have added value over a decimal type?
Is it just for special fractions like 1/3. If so, what is a more specific use case where distinguishing 1/3 from 0.333333333333333333 is important?
Just trying to better understand the specific expected gains here.
All benefits come from its accuracy and applicability. The decimal values are but a small subset of the rational numbers. Most rational numbers are not representable in binary or decimal digits. So I see that in human culture, with currencies and such, the decimal brings convenience. And I think that convenience could be extended and generalized.
The purpose is mostly for comparisons of (non-)equality, resolving in Boolean values, used for conditionals. I don’t think the intermediate rounding errors trickle down to a big problem, least not compared to the loss of performance.
I think NaN, infinity and overflow are cases where the developer should consciously choose for these drawbacks. You get the advantage of a fixed bit size.
Rational types can do everything a decimal type can do, just by only allowing the denominator be a power of 10. Choosing when to round - after each operation? - is an implementation detail, though it may be important.
I do think decimals are a good fit for the language to be “delightful”, since many cases can certainly be covered indeed.
And by extension I’d argue, how would you like to confidently be able to assess ‘x == 1/3’? Now you’d perhaps assess whether it is in some small margin of the fraction. Is that delightful?
For every decimal fraction, there exists an infinite amount of non-decimal fractions. The suggestion is really to ‘complete the set’ or ‘close the gap’.
Just a general note, our decimal is fixed point, so it has none of the issues related to infinity and Nan. It of course can overflow because it is a fixed number of bits (so would a rational with fixed bits)
Assuming rational would be limited to 2 u64s for performance reason, both our decimal and rational would be able to represent a similar range from minimal (closest to 0) to maximal value. The rational would just represent numbers that are not divisible by 2 or 5 better. This would come at the cost of needing to do unoptimized simplifying divisions. Because we know the base of our decimal type, any required division to get to the right base is very fast (gets turned into a multiplication with bit shifts)
Otherwise, my thought is that this sounds reasonable and should just be a user land library. I don't think it would have any requirements or major gains from being part of the language
Martin Stewart said:
I agree that the numerator and denominator should be simplified after each operation since custom equality isn't needed then. But what I'm getting at is that something like
1/2 + 1/3 + 1/5 + 1/7 + 1/11 + ... + 1/prime_nwill result inn/(2*3*5*7*11*...*prime_n)which eventually overflow the denominator and I'm wondering what the rational type will do then?
In order to relax the overflow problem, we could store a rational as a list of prime factors with their exponents. 1/7 could be (7,-1) and 1/2 + 1/3 + 1/5 + 1/7 would be (2,-1), (3,-1), (5,-1), (7,-1), (13,1), (19,1).
Where does the (13,1) and (19,1) come from?
Also, I would think that would have very poor performance, but it would definitely be hugely flexible if that is what is needed.
If I calculated correctly, it would be 247/210 or (13*19)/(2*3*5*7)
And yes, it would be slower than storing just numerator and denominator...
I'm not even sure how to efficiently compute the values for the primitive operations :sweat_smile:
Ah, So just different rational than the one from the equation...got it.
Pit Capitain said:
I'm not even sure how to efficiently compute the values for the primitive operations :sweat_smile:
I think RSA encryption is based on that fact that there no known way to efficiently find the prime factors for a large number? I'm not sure how large the number have to be for this to matter though.
those numbers are very very large, for most rational numbers it's not a concern
I saw in another thread that the float will be renamed to a frac. Is this related to the discussion above?
I could be wrong, but i think Frac a will be similar to Num a. It will be a grouping that includes multiple possible types (F32, F64, Dec)
yes, exactly!
Last updated: Jun 16 2026 at 16:19 UTC