Stream: ideas

Topic: rational type


view this post on Zulip Kas Buunk (Apr 26 2022 at 14:42):

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

view this post on Zulip Kas Buunk (Apr 26 2022 at 14:43):

Hi all, as per Richard's request I've pasted my idea here. Hope it's clear.

view this post on Zulip Martin Stewart (Apr 26 2022 at 18:52):

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.

view this post on Zulip Kas Buunk (Apr 27 2022 at 11:46):

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?

view this post on Zulip Martin Stewart (Apr 27 2022 at 11:59):

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.

view this post on Zulip Kas Buunk (Apr 27 2022 at 12:03):

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?

view this post on Zulip Martin Stewart (Apr 27 2022 at 12:11):

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.

view this post on Zulip Martin Stewart (Apr 27 2022 at 12:12):

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

view this post on Zulip Kas Buunk (Apr 27 2022 at 12:22):

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.

view this post on Zulip Kas Buunk (Apr 27 2022 at 12:36):

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.

view this post on Zulip Kas Buunk (Apr 27 2022 at 12:38):

And, multiplicative arithmetic could be as fast or faster, but addition not so much, is my intuition.

view this post on Zulip Brendan Hansknecht (Apr 27 2022 at 12:45):

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.

view this post on Zulip Kas Buunk (Apr 27 2022 at 18:07):

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.

view this post on Zulip Kas Buunk (Apr 27 2022 at 18:09):

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.

view this post on Zulip Kas Buunk (Apr 27 2022 at 18:11):

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.

view this post on Zulip Kas Buunk (Apr 27 2022 at 18:15):

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?

view this post on Zulip Kas Buunk (Apr 27 2022 at 18:30):

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

view this post on Zulip Brendan Hansknecht (Apr 27 2022 at 23:12):

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)

view this post on Zulip Brendan Hansknecht (Apr 27 2022 at 23:13):

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

view this post on Zulip Pit Capitain (Apr 28 2022 at 11:06):

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_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?

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

view this post on Zulip Brendan Hansknecht (Apr 28 2022 at 16:53):

Where does the (13,1) and (19,1) come from?

view this post on Zulip Brendan Hansknecht (Apr 28 2022 at 16:53):

Also, I would think that would have very poor performance, but it would definitely be hugely flexible if that is what is needed.

view this post on Zulip Pit Capitain (Apr 28 2022 at 17:00):

If I calculated correctly, it would be 247/210 or (13*19)/(2*3*5*7)

view this post on Zulip Pit Capitain (Apr 28 2022 at 17:01):

And yes, it would be slower than storing just numerator and denominator...

view this post on Zulip Pit Capitain (Apr 28 2022 at 17:04):

I'm not even sure how to efficiently compute the values for the primitive operations :sweat_smile:

view this post on Zulip Brendan Hansknecht (Apr 28 2022 at 17:04):

Ah, So just different rational than the one from the equation...got it.

view this post on Zulip Martin Stewart (Apr 28 2022 at 20:20):

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.

view this post on Zulip Ayaz Hafiz (Apr 28 2022 at 20:23):

those numbers are very very large, for most rational numbers it's not a concern

view this post on Zulip Kas Buunk (Aug 27 2022 at 04:49):

I saw in another thread that the float will be renamed to a frac. Is this related to the discussion above?

view this post on Zulip Brendan Hansknecht (Aug 27 2022 at 06:22):

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)

view this post on Zulip Richard Feldman (Aug 27 2022 at 11:59):

yes, exactly!


Last updated: Jun 16 2026 at 16:19 UTC