Hacker News new | ask | show | jobs
by pansa2 847 days ago
Note the top comment by “ark” - there’s really no perfect solution here.

In the floating-point case, you have to choose between negative remainders or potentially inexact results. And you definitely want integer division to work the same as float division.

3 comments

Python's floating point flooring division operator also has a pitfall: it gives a correctly rounded result as if you computed (a / b) with infinite precision before flooring. This can lead to the following:

    >>> 1 / 0.1
    10.0
    >>> 1 // 0.1
    9.0
This is because 0.1 is in actuality the floating point value value 0.1000000000000000055511151231257827021181583404541015625, and thus 1 divided by it is ever so slightly smaller than 10. Nevertheless, fpround(1 / fpround(1 / 10)) = 10 exactly.

I found out about this recently because in Polars I defined a // b for floats to be (a / b).floor(), which does return 10 for this computation. Since Python's correctly-rounded division is rather expensive, I chose to stick to this (more context: https://github.com/pola-rs/polars/issues/14596#issuecomment-...).

In an abstract sense, floating-point division is a fundamentally different operation than integer division. There is not generally expected to be a remainder fron floating-point division aside from 0.5 ULP that will be rounded away.

If you use it without considering rounding modes carefully and then round to integer, you can get some funny results.

I mean "inexact results" is essentially float's life motto.

That said, I don't really see why you would necessarily want float and integer division to behave the same. They're completely different types used for completely different things. Pick the one that is appropriate for your use case.

(It seems like abs(a % b) <= abs(b / 2) might be the right choice for floats which is pretty clearly not what you want for integers. I also just learned that integer division // can be applied to floats in Python, but the result is not an integer, for some reason?)

to people who use floating-point math seriously, it's very important for floating-point results to be predictably inexact; if they aren't, floating point is at best useless and usually harmful

i also didn't know python supported // on floats

Sure, but the inexactness of this modulus operation would not be any more unpredictable than all other kinds of float inexactness. Unless you're talking about the case where you don't know whether your operands are ints or floats. But in that case, there are tons of other unpredictabilities. For example, a + b will round when adding (sufficiently large) integers-represented-as-floats while it will never round for integers. So if you take floating-point math seriously, knowing that you're actually dealing with floats is the first step.
it's true that it would be deterministic, but it would behave differently from the ieee 754 standard, which is at least surprising, which is another sense of the word 'unpredictable'. admittedly, floating-point math that is inexact in a surprising way is not necessarily useless, and to someone who isn't deep into numerical analysis, all floating-point math is inexact in surprising ways

still, it would have real pitfalls if numerical algorithms that give right answers in every other programming language gave subtly wrong answers in python (raising a division-by-zero exception is less troublesome)

> i also didn't know python supported // on floats

Like most surprising features in python, it would be terribly annoying if it didn't. Especially since you couldn't make sure the argument to the function you're writing wasn't a float. At that time, anyway.

int(x) // int(y)
This does something different. a // b tries to "fit" b into a as many times as possible. For example, 1.0 // 0.4 == 2.0 because 0.4 fits twice into 1.0 (with a remainder of 0.2). Though as the result should always be an integer, I'd argue that the result should actually be 2, not 2.0. But alas, it's not.

With your change, you calculate 1 // 0 instead, which crashes.

That said, I think checking isinstance(x, float) was always possible. (And nowadays, you can put a type annotation.)

yes, agreed