Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mixing numerical types in other languages #1

Closed
berdario opened this issue Oct 8, 2015 · 5 comments
Closed

Mixing numerical types in other languages #1

berdario opened this issue Oct 8, 2015 · 5 comments

Comments

@berdario
Copy link

berdario commented Oct 8, 2015

Other languages that have floats and ints do not have this behavior

This might not be completely true, ruby:

irb(main):001:0> x = (1 << 53) + 1
=> 9007199254740993
irb(main):002:0> x + 1.0 < x
=> true

By simply trying to evaluate 9007199254740993 + 1.0 < 9007199254740993 in other languages, indeed I haven't been able to find another example... I tried for now:

  • Haskell (well, duh... you cannot compare int and fractionals in it)
  • Clojure (actually I tested the JVM floats, I suppose)
  • Perl5
  • NodeJS (Ok, ecmascript doesn't have integers, but I wanted to try nonetheless)
@waveform80
Copy link

I'm afraid any other language that implements 64-bit integers and 64-bit floats will indeed exhibit similar (though not necessarily precisely the same) behaviour. It ultimately depends how they handle casting for comparison operators...

The issue stems from the fact that (1<<53) + 1 evaluates to 9007199254740993. This has 16 (decimal) digits in it and fits happily in the range of a 64-bit integer. However, 64-bit floats only guarantee 15 digits of accuracy, and in this case converting the value to a float yields 9007199254740992. You can try adding one to that but obviously you'll just get the same value back (remember that it can't accurately represent that last digit so larger additions may work, but +1 won't).

We can demonstrate this with good old fashioned C:

#include <stdio.h>

int main(int argc, char *argv[]) {
    long long x = (1L << 53) + 1;

    printf("x = %lld\n", x);
    printf("(float)x = %f\n", (float)x);
    printf("x + 1.0 < x = %d\n", (long long)(x + 1.0) < x);

    return 0;
}

Note that the (long long) cast in the final printf is necessary to emulate Python's behaviour. Without that C coerces the right x operand to a float and the comparison returns negative (incorrectly because the right hand side of the comparison is indeed greater than the implicitly float coerced left).

Basically, Python's implicit casting behaviour is more correct here (though whether it's casting the float to a 64-bit int or casting both operands to something capable of accurately representing both like a Fraction or a Decimal instance, I don't know -- I'd hope the latter).

@cosmologicon
Copy link
Owner

Sure, if you add an explicit cast to make C emulate Python's behavior, it'll give the same result. But if you use the implicit behavior (just put x + 1.0 < x), it won't. In my opinion, adding the explicit cast makes it clear enough what's going on, which is why I personally would not consider this a wat in C.

though whether it's casting the float to a 64-bit int or casting both operands to something capable of accurately representing both like a Fraction or a Decimal instance, I don't know -- I'd hope the latter

Based on what I remember from the Python source code, it's neither. When you compare a float to an int in Python, it doesn't convert them to a common type at all. There's special logic for this case to ensure an accurate result. I guess Ruby behaves the same way.

@waveform80
Copy link

Hmm ... except in C, from one perspective you might convince yourself it's returning the wrong answer, and Python's returning the right one. It all depends on whether you accept the limitations of floating point types or not. I'll admit it's a bit of a convoluted argument, so I beg your indulgence with the following:

x = (1 << 53) + 1

At this point x is 9007199254740993 which is a fine value for a 64-bit integer (in Python or C).

x + 1.0 < x

Now ... we've explicitly specified we're adding a floating point so presumably we're expecting the LHS of this to be a floating point. However, due to the limitations of the floating point format, x is first converted to 9007199254740992.0 and adding 1.0 to it also returns 9007199254740992.0 (after all, if 9...3 converts to 9...2, then 9...2 + 1.0 must also result in 9...2).

So at this point in both Python and C we're comparing:

9007199254740992.0 < 9007199254740993

Python (and Ruby) do something (I haven't peeked under the covers to find out what yet) to ensure that it correctly returns True. C converts the RHS to a float (which naturally winds up being 9007199254740992.0 as above) and returns False (simply because x < x is False).

So, it's a "wat" in as much as intuitively x + 1 < x should always be False. But you're only getting the correct answer in C due to the operator being strictly less-than. Intuitively x + 1 <= x should always be False as well, but remove the cast in C and change the operator to "<=" and you'll get True back (because x <= x is True).

You will in Python too, but then that's the price of explicitly selecting a limited precision system like 64-bit floating point by writing + 1.0 instead of + 1 (which would stick with Python's "unlimited" integers). In other words, the "wat" stems from float's limited precision (specifically, the 53-bit mantissa) ... but that's not really Python's fault.

@cosmologicon
Copy link
Owner

Oh, don't get me wrong. I definitely think Python gives the "right" answer, in that I wouldn't change it at all. I think that Python treats these numerical types very reasonably. I think the "price" here is well worth paying. The whole point of the wat is that even reasonable behavior can lead to surprising edge cases when you take them out of context.

Intuitively x + 1 <= x should always be False as well

Well, I tend to think that most people who have any familiarity with floating-point numbers will not be surprised that x + 1 == x is sometimes true. To me, that seems like an obvious side effect of finite precision. Just my humble opinion, though.

@cosmologicon
Copy link
Owner

Changed the statement that this bug was initially about to also include Ruby. Thanks for the report!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants