3 ms·
Most languages don't have a coherent theory of what "==" means. Consider all the well-known problems in JavaScript and PHP, where "==" isn't even symmetric and
by StefanKarpinski 13y ago
Most languages don't have a coherent theory of what "==" means. Consider all the well-known problems in JavaScript and PHP, where "==" isn't even symmetric and certainly isn't transitive. Equality isn't transitive in C or Java either and in Python and Ruby it's a bit of a free-for-all. Most languages can sweep their lack of a theory for "==" under the carpet simply by saying "here are the rules, learn 'em" since you can't create your own numeric types or add behaviors to "==". In Julia, since anyone can add new numeric types that "==" will apply to, we need something better: we need to be able to tell people exactly what "==" means so that they can correctly implement it for their own types. This choice has deep implications for arithmetic (does "x == y" if and only if "x-y == 0"?), hashing and promotion, among other things.
The particular thread you're quoting was about a change (by me) that briefly altered the promotion behavior of rational numbers (and was reverted before any harm was done). It was fairly rapidly concluded that the existing behaviors of ints and floats were correct and that rationals should fall in line with those. Specifically, the correct behavior of "==" is that it must be transitive ("x == y" && "y == z" ==> "x == z"), but "x-y == 0" may occur in cases where "x != y" (e.g. when "x = 2^53+1; y = 2.0^53").
- stephencanon 13y agoCan you give an example of “==" failing to be transitive in C? The standard defines “==“ as a predicate that tests the numerical equality of the values of the (promoted) operands; unless you can hide something very clever in the promotion rules, that’s going to make the relation transitive. [Edit: as Stefan points out, there are cases where C’s promotion rules cause values to be rounded (which should have been obvious) — int32 to float32 and int64 to float64 in particular; this naturally leads to transitivity failures]. I’m also curious about your distinction between x == y and x - y == 0. Those are equivalent expressions for all reasonable integral and floating-point types (they fail to be equivalent when floating-point types lack denormals, but that’s “not reasonable”).
- StefanKarpinski 13y agoThe issues come in the mixed type cases. Consider this C program: #include <stdio.h> int main(void) { long x = 9007199254740992; // 2^53 double y = 9007199254740992.0; // 2.0^53 long z = 9007199254740993; // 2^53+1 printf("x == y: %d\n", x == y); printf("y == z: %d\n", y == z); printf("x == z: %d\n", x == z); return 0; } Its output shows that "==" is not transitive in C: x == y: 1 y == z: 1 x == z: 0 On the other hand, "==" is transitive in Julia: julia> x, y, z = 9007199254740992, 9007199254740992.0, 9007199254740993; julia> x == y, y == z, x == z (true,false,false) You have to be careful: if you compare "y" and "z" by converting "z" to Float64, they appear to be equal even though they represent entirely different integer values. When subtracting them, however, one does convert them to Float64, so their difference is zero: julia> z - y 0.0 This kind of issue is why it's so important to have a theory of what "==" means.
- stephencanon 13y agoAh, of course. So what you’ve essentially done is to make it so that comparisons and arithmetic use distinct promotion rules; comparisons are done without conversions that change value, whereas conversions are applied for arithmetic? Transitivity failures that arise from clearly specified conversion rules are at least easy to understand once you encounter them. If I understand correctly, to avoid losing transitivity, you’ve introduced a divergence between how types behave in arithmetic and comparisons, and broken the equivalence between x - y == 0 and x == y. To my mind as a numericist and low-level programmer, that’s much worse; it’s the path that Excel started to follow a long time ago, and it leads to all sorts of maddening details when corner cases collide. My preferred solution for high-level languages is “promote the operands to a type that can represent both exactly” (for the case of double and int64, this would be a floating-point type with at least 64 bits of significand and 11 exponent bits, like the Intel 80-bit format or quad). For a compute-oriented language, there are good performance reasons to avoid that, but it’s definitely the solution that complies with the principle of least surprise.