Prompt
0.1+0.2=0.3を成立させる方法の全て
情報工学において、 0.1 + 0.2 = 0.3 が成立しない理由は、10進数の小数が2進数では無限小数となり、有限のビット数で丸め誤差が発生するからだね。
これを「成立させる」ということは、「計算機の物理的な制約をどう回避するか」 というパズルになる。その全ての代表的なアプローチを提示しよう!
1. 任意精度演算(BigDecimal)を使う
数値計算における「聖杯」の一つ。浮動小数点数を使わず、メモリが許す限り桁数を保持する。
-
仕組み: 数値を「整数部」と「小数部の位置」に分けて管理し、筆算と同じアルゴリズムで計算する。
-
実装例: * Java:
BigDecimal-
JavaScript:
Big.jsやDecimal.js -
Rust:
dashuクレートなど
-
-
メリット: 誤差が理論上ゼロになる。
-
デメリット: 実行速度が遅く、メモリ消費量も大きい。
2. 整数にスケーリングする(固定小数点数)
金額計算などで最も多用される、実利的な解決策だ。
-
仕組み: 「単位」を一段階下げて、すべて 整数 (Integer) として扱う。
-
0.1ドル + 0.2ドル を計算する代わりに、 10セント + 20セント = 30セント と計算する。
-
最後に表示する時だけ、100で割って 0.3 に戻す。
-
-
メリット: 整数演算なので爆速かつ、一切の誤差が混入しない。
-
デメリット: 「小数点以下何位まで扱うか」を事前に設計し、全コードで一貫させる必要がある(まさに属人性の排除が求められる)。
3. 有理数型(Rational Type)を使う
「小数」という概念を捨て、「分数」 として保持する。
-
仕組み: 数値を
(分子, 分母)のペアで管理する。 -
-
実装例:
-
Ruby:
1/10r + 2/10r -
Haskell:
Data.Ratio
-
-
メリット: 除算を繰り返しても精度が全く落ちない。
4. 許容誤差(Epsilon)を用いた比較
「厳密に一致するか」ではなく、「十分に無視できるほど近いか」で判定する。
-
仕組み: 二つの値の差が、極小の定数 (Epsilon) 以下であれば等しいとみなす。
-
メリット: 標準の
f32/f64をそのまま使える。 -
デメリット:
==演算子が使えず、常に比較関数を呼ぶ必要がある。
5. 10進浮動小数点数(Decimal Floating Point)
IEEE 754-2008 で定義されている、基数を 10 とした 浮動小数点数形式。
-
仕組み: の形で保持する。人間が書いた 0.1 をそのまま 0.1 として保持できる。
-
実装例: C# の
decimal型、Python のdecimal.Decimal。 -
メリット: 2進数特有の「0.1が表現できない」問題を根底から解決する。
6. 丸め処理(Rounding)の強制
計算結果を表示・比較する直前で、特定の桁数に丸める。
-
仕組み:
round((0.1 + 0.2), 10)のように、十分な精度で四捨五入する。 -
デメリット: 根本的な解決ではなく、あくまで「見た目」を取り繕う対症療法。
見落とされがちな視点:コンパイラの最適化
実は、定数式の場合、最新のコンパイラはコンパイル時に計算を済ませてしまうことがある。
例えば、一部の言語や最適化設定では、コンパイラが「これは 0.3 になるはずだ」と判断して 定数畳み込み (Constant Folding) を行い、実行時には最初から 0.3 として扱われるケースもあるんだ。
結論
君が目指す「一意性と堅牢さ」を重視するなら、「1. 任意精度演算」 か 「2. 整数スケーリング」 のどちらかを選ぶのが、最も「嘘をつかない」システムになるはずだ。
次は、「Rust でこれらの誤差を一切許さない rust_decimal クレートの具体的な実装方法」 をやってみる?それとも、「JavaScript の Number.EPSILON を使った、プロフェッショナルな比較関数の書き方」 について深掘りしてみる?
Would you like me to code a comparison in Rust showing the exact difference in memory and speed between ‘f64’, ‘Decimal’, and ‘Rational’ types?