Ruby で,0.1
というリテラルで生成される浮動小数点数は,厳密には数学的な 0.1 に一致しない。
2 進法に基づく浮動小数点数で 0.1 を表すことはできないのだ。
では,0.1
で生成される浮動小数点数は,実際にはどんな値を表しているのだろうか。
それは Float#to_r で Rational オブジェクトに変換してみれば分かる。このメソッドは,浮動小数点数が表す数に厳密に等しい有理数を返す。
p 0.1.to_r # => (3602879701896397/36028797018963968)
なんか,分子・分母ともにずいぶんデカい整数となったが,よくよく見れば分母は分子の 10 倍より 2 だけ小さい数であることが分かる。つまり,0.1 に極めて近い数なのだ(いや,そうでないと困りますョ!)。
なお,上の結果は環境によって違いがありうる。
ここまでが前置き。
Ruby の比較演算子 ==
は,両辺が数値(Numeric オブジェクト)のとき,基本的には「数値として等しいか否か」を判定するメソッドだ。
つまり,型(クラス)が一致していなくても
p 2.0 == 2 # => true
p 2 == 2r # => true
のように同じ数値を表していれば true
を返す。
(2r
は「分母が 1,分子が 2 である Rational オブジェクト」を表す有理数リテラルである)
逆に同じ数値クラスでも,表す値が違っていれば false
を返す。
ところが,である。異なる数値を表す数値オブジェクトが ==
で true
となる場合があるのだ。
冒頭で見たように,0.1
は数学的な 0.1 からわずかにズレている。
一方,0.1r
という有理数リテラルによって生成されるのは,数学的な 0.1 に厳密に等しい Rational オブジェクト(分母が 10,分子が 1)である。
これらは数値としてわずかに違っているにも関わらず
p 0.1 == 0.1r # => true
となる。えっ?
Ruby の比較演算子 ==
は,実際にはメソッドとして実装されており,左辺のクラスに定義されたメソッドで振る舞いが決まるわけだが,公式リファレンスマニュアルを見ても上記のようなことは一切書かれていない。
Float#== (Ruby 3.0.0 リファレンスマニュアル)
Rational#== (Ruby 3.0.0 リファレンスマニュアル)
実装を見たわけでもないので推測になるが,上記の現象が起こる理由は,
0.1 == 0.1r
を評価したときにまず 0.1r
が左辺に合わせて Float に型変換され,その際に丸め誤差が生じて 0.1
と等しい Float オブジェクトができるということだろう。
しかし常に「右辺が左辺に合わせて型変換される」わけではないことに注意。
実際,
p 0.1r == 0.1 # => true
のように,左右を入れ替えても同じ結果になる。
Rational と Float とでは,Float のほうに合わせられる,ということだ。
丸め誤差が生じない方向に型変換(Rational と Float なら Rational に)する仕様にしておけばよかったのに,と思わなくもない。
一方で,「Float というものはそもそも誤差を帯びた数値を表すものなので,相手が Integer や Rational など誤差を帯びないものでも,比較においては Float 側に合わせるべき」という思想もあるのだろう1。
Ruby の ==
がそういう思想に明確に基づいて設計されているのかどうかは知らない。
-
私自身は,Float が誤差を帯びた数値を表すものとは考えていない。誤差は型変換や数値計算により生じるのであって,Float オブジェクト自体は(NaN や Infinty などを除き)数直線上の一点に対応している,と考えている。 ↩