1. IMO it's unfortunate that most languages default to floating-point. Most programmers, most of the time, would be better served by slightly slower but less confusing alternatives (it's nice that Raku uses rational numbers by default: similarly for integers, it's great that Python uses arbitrary-precision integers by default). At any rate, programmers would be a lot less confused about floating-point arithmetic if they had to opt in to it explicitly, e.g. instead of 0.1 + 0.2 if they had to say something super-explicit like (just exaggerating a bit for effect, this is probably impractical anyway):
NearestRepresentableSum(NearestRepresentable("0.1"), NearestRepresentable("0.2"))
till they got the hang of it.2. IMO when explaining floating-point arithmetic it helps to add a picture, such as this one (added to Wikipedia by a user named Joeleoj123 in Nov 2020): https://upload.wikimedia.org/wikipedia/commons/b/b6/Floating...
With this picture (or a better version of it), one can communicate several main ideas:
- There are a finite number of representable values (the green points on the number line),
- Any literal like "0.1" or "0.2" or "0.3" is interpreted as the closest representable value (the closest green point),
- Arithmetic operations like addition and multiplication give the closest green point to the true sum,
- There are more of them near 0 and they get sparser away from 0 (the "floating-point" part),
etc.
Further, by staring at this picture, and the right words, one can infer (or explain) many of the important properties of floating-point arithmetic: why addition is commutative but not associative, why it is a good idea to add the small numbers first, maybe even the ideas behind Kahan summation and what not.
Most languages, most of the time, are not used by beginners.
However, if you use "beginner" to the knowledge gained, it might not be. If you only ever make webpages, even if you have made 100s you could still be a novice programmer because you never branched out enough to learn new programming concepts.
If a programming language makes it easy to do something moderately, but it is hard to do it well. Programmers who only know that language are likely to have a gap in their stills. Floating point (in most languages) is easy to gets the basics working, but are hard to do well and very hard to do perfectly. This leads to a lot of programmers not learning floating point well.
If you want to force usage of floating point arithmetic, you will have to indicate that in literal values. E.g. `0.1` would be a [Rat](https://docs.raku.org/type/Rat) (Rational number), and `0.1e0` would be a [Num](https://docs.raku.org/type/Num) (aka a floating point".
In Raku, to get the nearest representation, use the [.narrow](https://docs.raku.org/routine/narrow) method. So `42e0.narrow`, `42.0.narrow` and `42+0i.narrow` would all be an `Int`. Yes, Raku has complex numbers built in as well :-)
Ultimately, there is no substitute for knowing what you are doing.
The only sentence I don't really like:
> When you have a base-10 system (like ours), it can only express fractions that use a prime factor of the base.
It's a weird mix of over- and under-generalization. The second half sounds like a feature of all number systems independent of the base, and we can express more numbers (just with a notion of 'repeating infinitely' or as fractions), that's why they even switch to "expressed cleanly" in the next sentence.
If I get more sleep and can think of a good way to express it, maybe I'll actually make a pull request ;)
IMO it could do better to explain why this is the case rather than state it as a fact, as it’s not immediately obviously true (at least to me).
A terminating decimal is equivalent to a fraction with a denominator that is a power of 10. Any fraction with a denominator that is a product of prime factors of 10 can be turned into a fraction with a denominator that is a power of 10. Thus the only fractions with terminating decimal representations are those with denominators which are a product of prime factors of 10.
Of course this is true for any base other than 10, but I couldn’t think of a term for “terminating decimal” with other bases.
> Any fraction with a denominator that is a product of prime factors of 10 can be turned into a fraction with a denominator that is a power of 10.
isn’t sufficient. You also have to argue that any fraction with a denominator that is not a product of the prime factors of 10 cannot be written as a finite decimal.
If you do that you end up with a tautology.
A better and more rigid explanation would start with a rational p/q, with p and q relatively prime that can be written as some integer divided by 10^n, and reason from there.
I imagine you could create a table of the fractions 1/2, 1/3, ..., 1/10; prime factorization (e.g. 1/4 = 1/2 * 1/2), their decimal representation, their binary representation, and maybe for familiarity the sum-of-fractions represented by the binary representation. e.g. 0.101 meaning 1/2 + 1/8.
0.1 and 0.2 = 0.3 - https://news.ycombinator.com/item?id=28390997 - Sept 2021 (2 comments)
0.30000000000000004 - https://news.ycombinator.com/item?id=21686264 - Dec 2019 (402 comments)
0.30000000000000004 - https://news.ycombinator.com/item?id=14018450 - April 2017 (130 comments)
0.30000000000000004 - https://news.ycombinator.com/item?id=10558871 - Nov 2015 (240 comments)
However, in reality most numbers I deal with can reasonably be assumed to have a maximum number of decimals and can be stored easily this way. I know way fewer examples where I'd need floating point precision than where I need a fixed number of decimals. But with poorer support I always fall back to using float.
But in the end I tend to use that for most numbers, especially as compression in time series also often better works on integers than floats if you have gradual changes in the numbers.
So, if you want to hold on to that idea, the question becomes what numbers best to pick.
If you go for rational numbers, you soon discover that addition of rationals is slow because you have to [1] multiply the denominators and find a greatest common divisor (example: 1/3 + 1/9 = (19 + 13)/(3*9) = 12/27 = 4/9)
You also will find that your denominators can rapidly get large, and that, in real life, you need to represent a fairly wide range of numbers (say at least between 10^-3 and 10^6). That means you either accept large, random rounding in calculations, or have to pick a large number of bits
Also, the math you have to do to find the closest rational for square roots and results of goniometric functions makes those functions slow.
So, you either have to give up the idea to store rationals, or the idea to store every number in the same finite number of n bits. IEEE chooses the former for performance reasons.
[1] assuming you want to have a single representation for each rational. You likely want, as you don’t want to waste space and because you want equality testing to be simple.
Say we have a 32 bit CPU. Let's store fractions in two halves, the numerator and denominator.
This turns out to be super inconvenient for irrational numbers. We can use an alternative representation where the number is in two halves, the integer half and fractional half. In other words your first 32 bits are 1X2^0...31 and second 32 bits are 1x2^-1...-32. This is called fixed point.
You run into a problem where this is usually extremely wasteful in the number of bits used, so you can fuse them into a single 32 bits used, and the binary decimal point is at the 16th bit, 18th bit, whatever you need. You just need to track it when multiplying or dividing numbers to handle the appropriate shifts. This is "fixed" point math, and it's what we did for decades because it's super cheap in hardware (it's the same integer adders/multipliers, just some extra work for division and shifting for multiplication).
You might even want the decimal point to move. Say your numbers are almost always less than 1, why use more than 1 bit for the integer part and in software, handle the overflows and change of decimal point when needed?
Well that software part is super complex, so you can implement some special hardware to do it. It's even really convenient to change the representation from a fixed integer and decimal half to a decimal mantissa and integer exponent. Now you've reinvented floating point. It would be great if there was a standard so all languages could agree on representation and hardware to implement it really fast. That's IEEE 754.
However, this is not enough in many cases. For example, how would you store pi? In the case of Lisp, pi is stored as a floating point number, and the conversion rules say that a mathematical operation between a rational and a floating point number yields a floating point number. This means that even though you have rational numbers, you still have to be aware of floating point.
Of course, ultimately this is going to be a compromise and I understand the choice of not storing common constants that way. In the end you'd end up just replicating common mathematical notation and computing with that is not going to be fast.
how would you store pi?
same way we do now I guess? Approximate it. My vote is on 22 / 7Also keep in mind that because computing is a growing field, the majority of people in it are relatively new to the field, plus there are plenty of people here that don’t have CS degrees.
FWIW, there are many representations for real & floating point numbers. Rationals are an example of something that’s neither floating point nor BCD, and can represent 1/10 and 1/3 exactly. Here’s a fun example: https://iquilezles.org/articles/floatingbar/
But I also don't think it is particularly 'news', the discussion on previous versions centered around the problems that arise from floats, such as inconsistent handling by tools and languages and 'inapropriate use' where the precision from floats is not good enough or misleading. So maybe it is more a "topic of interest".
1) it's really slow slow. Adding two fractions involves three integer multiplications and then running the GCD to simplify the fractions.
2) just simply comparing two rational numbers involves multiplication, which may overflow.
3) you can't represent irrational numbers. You can't do square roots, for example.
Hard disagree right from the start - if your language is doing floating point math by default (as in, if numeric literals and basic operators use floating point in the absence of explicit type annotations or tags), your language is broken. Maybe if it's hyper-targeted towards graphics or ML or some other field where floating point errors don't matter and performance is king, then I'd call it fine, but users of such languages probably aren't reading this page.
Now, I get what the author is saying - there isn't a bug in your interpreter, it's how it's designed. But that means it's broken by design.