Hacker News new | ask | show | jobs
by GuiA 3502 days ago
Great writeup!

Notice that range stops just shy of 2π. Floating point math sometimes produces a final angle indistinguishably close to 2π, adding one extra vertex to our n-gon. To see for yourself, change (- tau 0.000001) to tau and set the number of sides to 6. You’ll see an extra point in the output very close to the starting point.

This is a "trick" that you find everywhere in graphics programming, and I can't believe we've been programming for over half a century and still have to deal with crap like that in our tooling. It leads to code that, while readable to the initiated programmer, is not what it should be.

3 comments

It wouldn't have been a problem if it instead used an index variable i ranging from 0 to the number of sides, computing 2pii/sides along the way.

If you're going to rely on repeated addition of irrational numbers to come out to some exact value, then you've misunderstood the abilities of finite decimal representations. It is something easy to overlook, but shouldn't be too surprising to someone who has computed anything by hand (for instance, try long dividing 1/7 out to some number of decimal places, and then adding the result to itself seven times --- if it's a problem there it's not simply a tooling issue, assuming of course the problem isn't with decimals!).

Though, if we want to fix our tools, perhaps instead of the promise of a "cos" or "sin" functions, we could have (ngon-point i n) which returns [(cos (/ i n)) (sin (/ i n))]. Then you wouldn't be tempted to use floating-point numbers to represent the index of a polygon vertex.

I fully agree.

As a more general strategy, just use integers (preferably unsigned integers) for all critical stuff such as state handling and looping over. Then, perform floating point operations only on top of that, as a last step. Sometime you don't even need that and use fixed point arithmetics instead (i.e. change your unit to some fixed fraction, e.g. in accounting calculate with integer cents instead of floating point euros/dollars).

> Then, perform floating point operations only on top of that, as a last step.

Right. "Once you go float, you never go back."

Any time you need discrete operations like counting and equality, do it in integer space. Avoid going from integer space back to floating point space.

We can do better. It's true that decimal representation doesn't work:

    0.999999 = 0.142857 + 0.142857 + 0.142857 + 0.142857 + 0.142857 + 0.142857 + 0.142857 
And yes, rewriting the problem in an integer-indexed form does get the correct result:

    1 = (1 + 1 + 1 + 1 + 1 + 1 + 1) / 7
But what we want, in both the original code and in this example, is to calculate:

        1   1   1   1   1   1   1
    1 = — + — + — + — + — + — + —
        7   7   7   7   7   7   7
That's even hard to type out in our limited format here on HN. But it's what we mean to do.

Maybe better tooling could infer that if the limited math produces an angle indistinguishably close to 2pi, we probably meant for it to be equal to 2pi. Maybe it could do fractional addition, instead of converting to a decimal representation and truncating the number of digits.

Maybe it would write the outputs of the function in the initial triangle example, instead of as

    ([50 0]
     [-24.99999999999999 43.30127018922194]
     [-25.00000000000002 -43.301270189221924])
it would be represented as

    ([50 0]
     [-25 25*sqrt(3)]
     [-25 -25*sqrt(3)])
Because the sine of 2pi/3 is sqrt(3)/2, not 0.866.
Floating point math is hard. I recommend this interview of William Kahan, creator of the IEEE-754 standard, and the associated HN discussion (esp. the comment by user russell)

https://news.ycombinator.com/item?id=7769303

Final angle? If it's gonna close the path for you, just stop at n-1. No extra side.