=begin
Integer 10 means exactly 10, not everything that would end up as 10 if rounded. 10 == 10.2 #=> false
but 10 == 10.0. 100000000000000000000000 should == 100000000000000000000000.0
You know, if you think it should not, you have to persuade us.
Assigning this to matz because it turned out to be a design matter.¶
To me 100000000000000000000000.0 means 100000000000000000000000.to_f,
or "floating point number closest to 100000000000000000000000".
This doesn't have to be exactly 100000000000000000000000.
Some arguments follow.
== Argument from other languages ==
This treatment of float equality seem to be unique to Ruby.
Compare with Python:
print(100000000000000000000000 == 100000000000000000000000.0) #=> False
print( 99999999999999991611392 == 100000000000000000000000.0) #=> True
print(Fraction(1,3) == 1.0/3.0) #=> False
print(Fraction(1,4) == 1.0/4.0) #=> True
With Perl it's more complicated, as Perl's standard number type
switches between native int, native float, and decimal the way Ruby switches
between fixint and bigint - so 1000000000000000000.0 in Perl is
decimal, not float and so exact.
Still, it doesn't follow "equal if converts".
This is the same regardless of $x being decimal or float internally.
my $x = 1000000000000000000;
my $a = Math::BigInt->new('1000000000000000000');
my $b = Math::BigInt->new('1000000000000000001');
print($x == $a->numify() ? "equal" : "not"); #=> equal
print($x == $b->numify() ? "equal" : "not"); #=> equal
print($x == $a ? "equal" : "not"); #=> equal
print($x == $b ? "equal" : "not"); #=> not
A counterexample to this would be C, which is quite explicit that
operations involving different numeric types involve implicit conversion.
It doesn't have bignums, but comparing float vs int or
double vs long long will convert and lose precision before comparison.
It also loses precision this way when comparing integers of
different signedness etc. - this mess is a good example of what
we shouldn't do ;-)
== Argument from sort ==
What would you guess this code to print?
Build array of bignums, add extra floats¶
big = 10**50
values = (1..10).map{|x| big + x}
values += (1..10).map{ big.to_f }
Make sure it's sorted¶
values.shuffle!
values.sort!
Just cleanup for printing¶
values.reject!{|x| x.is_a?(Float)}
values.map!{|x| x - big}
p values
If you guessed [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
you'd be wrong most of the time.
sort relies on transitivity of <=>, so if <=>
says big.to_f equals both big+1 and big+7,
it naturally assumes big+1 <=> big+7 will also be 0.
It's not a bug in sort - it's how <=> works for everything
except floats.
== Argument from mathematics ==
Ruby has a few equality-like relations - ==, eql?, equal?.
They differ, but they're all (almost) mathematical equivalence relations.
For all three, these can be expected:
- a == a
- a == b iff b == a
- if a == b and b == c then a == c
I can think of one good exception to a == a -
with things like float NaNs and sql nulls - but these really
are more techniques for handling errors than real values.
I really cannot think of a single case where it would
make sense to make equality either non-symmetric,
or non-transitive.
Similar reasoning applies to <=> - normally it defines
partial order between objects:
- a >= b and b >= a only if a == b
- if a >= b and b >= c then a >= c
Transitivity of equality, and transitivity of partial ordering
seem to be violated only in one case in Ruby - for comparisons
between floats and other numeric types. (and this causes sort
to break).
Can you think of any other type that does something like that?
== Argument from rationals ==
When you have mixed type operations, and one type is bigger,
the most obvious thing to do is simply converting argument
of smaller type to bigger type.
For example you can define pretty much every Rational vs Integer
operation as:
class Rational
def something(x)
x = x.to_r if x.is_a?(Integer)
...
And you can do this for Complex operations
and non-complex arguments etc.
You never do it the other way around -
converting to smaller type.
This would be obviously wrong:
class Integer
def something(x)
x = x.to_i if x.is_a?(Rational)
...
So why, if Rational can represent every floating
point value (except nans/infinities/negative zero that is),
do rational vs float operations downconvert to float,
instead of upconverting to rational?
I can think of no other such case anywhere.
With bignum vs float, neither is strictly wider than other,
but both can be represented as rationals.
This doesn't mean I want bignum + float to return rationals,
downconversion before returning is perfectly fine.
I just want it to be pretty much equivalent to this:
class Integer
def +(x)
if x.is_a?(Float) and x.finite?
return (self.to_r + x.to_r).to_f
...
== Argument from IEEE 754 ==
This .to_r/.to_f above might be puzzling, but look at this.
All basic floating point operations are defined by IEEE 754
standard as mathematically equivalent to this:
def +(x)
return (self.to_real + x.to_real).to_f(rounding_mode)
end
Where .to_real means conversion to actual mathematical
real number with potentially infinite precision, all extra
bits being 0s.
Of course it's not implemented like that - but the result
is guaranteed to be exactly the same as if it was.
== Other inaccuracies ==
I'm not really terribly bothered by that, only by equality
and <=>, but a lot of operations involving floats and other
types downconvert to float too early and lose precision.
puts(Rational(15,11)*11.0 == 15.0) #=> false
puts((Rational(15,11) * 11.0.to_r).to_f == 15.0) # => true
puts(100000000000000000000000 - 100000000000000000000000.0) #=> 0
puts((100000000000000000000000 - 100000000000000000000000.0.to_i).to_f) #=> 8388608.0
Such precision loss never happens for anything that involves
only floats, or only non-floats. And it really wouldn't be
that difficult to avoid it if we cared, but if even I don't,
I doubt others will.
=end