LoginSignup
10
7

More than 5 years have passed since last update.

浮動小数点数の順序には二種類あるよ(-0.0, +0.0, NaN を比較すると?)

Last updated at Posted at 2019-06-27

Scala 2.13 がリリースされましたね。
ここで、おもむろに Seq(3.0, 1.0, 2.0).sorted などと実行すると、

warning: object DeprecatedDoubleOrdering in object Ordering is deprecated (since 2.13.0): There are multiple ways to order Doubles (Ordering.Double.TotalOrdering, Ordering.Double.IeeeOrdering). Specify one by using a local import, assigning an implicit val, or passing it explicitly. See the documentation for details.

という警告が出るようになりました。 Double の順序型クラスとして TotalOrdering IeeeOrdering の二つが用意されているので明示的にどちらか一つを選びなさい、と言っていますね。
これは何でしょうか。なぜ順序に種類があるのでしょうか……?

Java

まずは Java で Double の比較について確認してみましょう。

jshell> 1.0 == 1.0               // true
jshell> 1.0 != 1.0               // false
jshell> Double.compare(1.0, 1.0) // 0
jshell> 2.0 > 1.0                // true
jshell> Double.compare(2.0, 1.0) // 1
jshell> 1.0 > 2.0                // false
jshell> Double.compare(1.0, 2.0) // -1

それはそう、という感じです。ここに紛れはありません。

それではみんな大好き NaN (Not a Number) を比較してみます。

jshell> 0.0 / 0.0                              // NaN

jshell> Double.NaN == Double.NaN               // false
jshell> Double.NaN != Double.NaN               // true
jshell> Double.compare(Double.NaN, Double.NaN) // 0
jshell> Double.NaN > 0.0                       // false
jshell> Double.compare(Double.NaN, 0.0)        // 1
jshell> 0.0 > Double.NaN                       // false
jshell> Double.compare(0.0, Double.NaN)        // -1

おやおや。等号・不等号と Double.compare に意見の相違が見られますね?
等号・不等号は Double.Nan を食ったらそれはまともな数ではないので != 以外では false を吐くと言っています。
対して Double.compare は、 Double.Nan とは 0.0よりも大きい数であると言っているのです。

等号・不等号の扱いの方が自然な気もしますが、これが問題になることもあって、

static boolean check(double d1, double d2) {
    return (d1 <= d2) || (d2 <= d1
}

という必ず true を返すことを期待したい関数が、

jshell> check(1.0, 2.0)        // true
jshell> check(2.0, 1.0)        // true
jshell> check(Double.NaN, 1.0) // false

false を返してしまうことがあるのですね。こうなるとアルゴリズムによっては、ソートが終わらなかったりします。これはいわゆる全順序律「任意の元 a, b について a ≦ b または b ≦ a」が成り立たない状況です。

対して Double.compare は、

static boolean checkCompare(double d1, double d2) {
    return (Double.compare(d1, d2) <= 0) || (Double.compare(d2, d1) <= 0
}
jshell> checkCompare(1.0, 2.0)        // true
jshell> checkCompare(2.0, 1.0)        // true
jshell> checkCompare(Double.NaN, 1.0) // true

Double.NaN を含めて全順序律が成り立っています。
※ ちなみに、これは IEEE 754 の totalOrder とは異なるようです。詳しくはコメント欄を読んでください。

この違いはどこから来るのか Double.compareの実装を確認してみると、

public static int compare(double d1, double d2) {
    if (d1 < d2)
        return -1;           // Neither val is NaN, thisVal is smaller
    if (d1 > d2)
        return 1;            // Neither val is NaN, thisVal is larger

    // Cannot use doubleToRawLongBits because of possibility of NaNs.
    long thisBits    = Double.doubleToLongBits(d1
    long anotherBits = Double.doubleToLongBits(d2

    return (thisBits == anotherBits ?  0 : // Values are equal
            (thisBits < anotherBits ? -1 : // (-0.0, 0.0) or (!NaN, NaN)
             1)                          // (0.0, -0.0) or (NaN, !NaN)
}

openjdk/jdk/src/java.base/share/classes/java/lang/Double.java

となっていて、なるほどなーというわけですが NaN 以外にも特別な場合がコメントされていますね。こちらも確認してみると、

jshell> -0.0 == +0.0               // true
jshell> -0.0 < +0.0                // false
jshell> Double.compare(-0.0, +0.0) // -1

ということで、等号は -0.0+0.0 が同じ数と言っていますが、 Double.compare-0.0 より +0.0 の方が大きい数と言っています。
え、 -0.0+0.0 を違う数として判定できるの?と驚くかもしれませんが、

jshell> Long.toBinaryString(Double.doubleToLongBits(-0.0)) // 1000000000000000000000000000000000000000000000000000000000000000
jshell> Long.toBinaryString(Double.doubleToLongBits(+0.0)) // 0

この通り -0.0+0.0 は異なるビット表現がされています。

他に Double の特殊な値として -∞ を表す Double.NEGATIVE_INFINITY と +∞ を表す Double.POSITIVE_INFINITY があるので合わせてまとめると、 Java の等号・不等号が定める順序では、

-∞ < -1.0 < -0.0 == +0.0 < 1.0 < +∞

Double.compare が定める順序では、

-∞ < -1.0 < -0.0 < +0.0 < 1.0 < +∞ < NaN

ということになります。

Scala

さて、ここまで来れば Scala の方も理解できます。
TotalOrdering IeeeOrdering の実装を覗いてしまうと、

trait TotalOrdering extends Ordering[Double] {
  def compare(x: Double, y: Double) = java.lang.Double.compare(x, y)
}
implicit object TotalOrdering extends TotalOrdering
trait IeeeOrdering extends Ordering[Double] {
  def compare(x: Double, y: Double) = java.lang.Double.compare(x, y)

  override def lteq(x: Double, y: Double): Boolean = x <= y
  override def gteq(x: Double, y: Double): Boolean = x >= y
  override def lt(x: Double, y: Double): Boolean = x < y
  override def gt(x: Double, y: Double): Boolean = x > y
  override def equiv(x: Double, y: Double): Boolean = x == y
  override def max[U <: Double](x: U, y: U): U = math.max(x, y).asInstanceOf[U]
  override def min[U <: Double](x: U, y: U): U = math.min(x, y).asInstanceOf[U]
}
implicit object IeeeOrdering extends IeeeOrdering

scala/scala/src/library/scala/math/Ordering.scala

となっていて TotalOrdering はその名の通り Java の Double.compare の全順序に従うのですが、 IeeeOrdering はメソッドによって異なる順序に従うことが分かります。ややこしいですね。

さて IeeeOrdering TotalOrdering で挙動がどう異なるか確認してみましょう。

scala> import Ordering.Double.TotalOrdering
scala> val seq = Seq(+0.0, Double.PositiveInfinity, Double.NaN, -0.0, Double.NegativeInfinity)
scala> seq.sorted
res0: Seq[Double] = List(-Infinity, -0.0, 0.0, Infinity, NaN)
scala> import Ordering.Double.IeeeOrdering
scala> val seq = Seq(+0.0, Double.PositiveInfinity, Double.NaN, -0.0, Double.NegativeInfinity)
scala> seq.sorted
res0: Seq[Double] = List(-Infinity, -0.0, 0.0, Infinity, NaN)

おや、同じ結果ですね。
これは Seq.sorted が依存しているのは Ordering.compare のため TotalOrderingIeeeOrdering も同じ順序となるからです。ふーむ。他のメソッドはどうでしょうか。

scala> import Ordering.Double.TotalOrdering
scala> val seq = Seq(+0.0, Double.PositiveInfinity, Double.NaN, -0.0, Double.NegativeInfinity)
scala> seq.min
res0: Double = -Infinity

scala> seq.max
res1: Double = NaN
scala> import Ordering.Double.IeeeOrdering
scala> val seq = Seq(+0.0, Double.PositiveInfinity, Double.NaN, -0.0, Double.NegativeInfinity)
scala> seq.min
res0: Double = NaN

scala> seq.max
res1: Double = NaN

おや、 min が違う結果ですね。
これは Seq.min が依存しているのは Ordering.min のため TotalOrderingIeeeOrdering は違う順序となるからです。なるほどなー。

ちなみに Double の順序型クラスを明示しなかった場合、Scala 2.13 では TotalOrdering
と同等のものが参照されますが、Scala 2.13 より前のバージョンで参照されるのは IeeeOrdering と同等のものなので、なんと挙動が変わっています。一応、確認してみると、

// Welcome to Scala 2.12.8 (OpenJDK 64-Bit Server VM, Java 11.0.2).

scala> val seq = Seq(+0.0, Double.PositiveInfinity, Double.NaN, -0.0, Double.NegativeInfinity)
scala> seq.sorted
res0: Seq[Double] = List(-Infinity, -0.0, 0.0, Infinity, NaN)

scala> seq.min
res1: Double = -Infinity

scala> seq.max
res2: Double = -0.0
// Welcome to Scala 2.13.0 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_181).

scala> val seq = Seq(+0.0, Double.PositiveInfinity, Double.NaN, -0.0, Double.NegativeInfinity)
scala> seq.sorted
           ^                                                                      ^
       // warning: object DeprecatedDoubleOrdering in object Ordering is deprecated (since 2.13.0): There are multiple ways to order Doubles (Ordering.Double.TotalOrdering, Ordering.Double.IeeeOrdering). Specify one by using a local import, assigning an implicit val, or passing it explicitly. See the documentation for details.
res0: Seq[Double] = List(-Infinity, -0.0, 0.0, Infinity, NaN)

scala> seq.min
           ^                                                       ^
       // warning: object DeprecatedDoubleOrdering in object Ordering is deprecated (since 2.13.0): There are multiple ways to order Doubles (Ordering.Double.TotalOrdering, Ordering.Double.IeeeOrdering). Specify one by using a local import, assigning an implicit val, or passing it explicitly. See the documentation for details.
res1: Double = -Infinity

scala> seq.max
           ^
       // warning: object DeprecatedDoubleOrdering in object Ordering is deprecated (since 2.13.0): There are multiple ways to order Doubles (Ordering.Double.TotalOrdering, Ordering.Double.IeeeOrdering). Specify one by using a local import, assigning an implicit val, or passing it explicitly. See the documentation for details.
res2: Double = NaN

あれ、今度は max が違う結果ですね。
これはさらにややこしいことに Scala 2.13 では Seq.min Seq.max の実装も変更されているためです。
ともかく Scala 2.13 に移行すると挙動が変わる可能性はあるので、 警告が出たら TotalOrderingIeeeOrdering のどちらが実装したい機能にふさわしいか考慮したうえで明記するのがいいでしょう。

10
7
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7