これは Ruby Advent Calendar 2019の18日目の記事です。Otemachi.rb #17 - connpassで話した内容の大幅加筆修正版です。
実用性は皆無ですので、求めている方はブラウザバックしてください。
NaN (Not a Number) とは
みなさん NaN ってご存知でしょうか?
NaNとは Not a Number の略で、日本語では非数といいます。主に浮動小数点演算の結果が不定形だったり未定義だったりするときに返ってきます。
constant Float::NAN (Ruby 2.6.0 リファレンスマニュアル)
その数がNaNであるかどうかは、Float#nan?
で調べることができます。
NaNの仕様はIEEE 754で規定されているので、大抵のプログラミング言語には実装されています。
JavaScriptだとたまに見かけますが、RubyだとNaNを返すのではなく例外を発生させるケースが多い1ため、ほぼ見かけないですね。
NaNが返ってくるパターン
他にもあったらコメント欄などで教えて下さい。
Float::NAN
を代入する
irb(main):001:0> nan = Float::NAN
# => NaN
そりゃそうだ
NaNと四則演算をする
Float::NAN + 1
# => NaN
Float::NAN - 1
# => NaN
Float::NAN * 1
# => NaN
Float::NAN / 1
# => NaN
上記のような四則演算に限らず、NaNと計算した結果はNaNになります。
つまり、NaNは感染します。
浮動小数点で不定形を計算する
「不定形」という言葉の意味、高校で理系数学をやった方はわかると思いますが、これを計算しようとするとNaNになります。
0/0.0
0/0.0
# => NaN
こうなるのは浮動小数点数だけで、整数で 0/0
とするとZeroDivisionError
が返ってきます。
この違いは、両者がもつ意味の違いではないかと思います。
整数の 0
は正確に0である一方で、浮動小数点数でx = 0.0
とすると0 <= x < 0.05
を満たす数を表す、という意味をもつ、という違いがあります。
0/0.0
はゼロ除算ではないのかもしれないけど、値を計算できないから非数となるのですね。
INFINITY/INFINITY
Float::INFINITY / Float::INFINITY
# => NaN
Float
クラスの中では「無限大」を表す Float::INFINITY
が定義されています。
ただ無限大を無限大で割ってもどの値に収束するかわかりませんね。これもNaNです。
INFINITY - INFINITY
Float::INFINITY - Float::INFINITY
# => NaN
こちらもNaNになります。
NaNのNaNa不思議
ここからは、NaNのNaNa不思議、もとい変わった性質や挙動を見ていきましょう。
1. 自分どうしを==
で比較するとfalse
が返ってくる
Float::NAN == Float::NAN
# => false
Rubyでこの挙動をする唯一の値ではないでしょうか。
これを利用した問題が、RubyKaigi 2019のエムスリーさんのブースで出題されてました。
難読Rubyコードクイズ問題と解説 in RubyKaigi 2019 - エムスリーテックブログ(Day3-3)
2. truthy
である
Float::NAN ? 'hoge' : 'piyo'
# => "hoge"
!!Float::NAN
# => true
「nil
とfalse
以外はすべてtruthy
」というRubyのルールにしたがって、NaNも真と判定されます。
これはそこまで変わった性質じゃないですが、どんな値と比べてもfalseになるのに、単独だときっちり真と判定されるというのは直感的にはなんだか不思議です。
3. 複素数に変換できる
Float::NAN.to_c
# => (NaN+0i)
お役所仕事という感じがします。
4. 複素数どうしで0.0/0.0
をすると実部も虚部もNaNになる
Complex(0.0, 0.0) / Complex(0.0, 0.0)
# => (NaN+NaN*i)
複素数つながりでもう一個。
複素数の割り算は分母の共役複素数を分母分子にかけて分母を実数にする、と教わりましたが、分母のゼロになにをかけてもやっぱりゼロです。
ちなみに、コメント欄で @scivola さんから指摘があったとおり、0.0/0.0
以外でも同じ結果になる場合があります。
1/(0.0i)
# => (NaN+NaN*i)
5. 数値なのにsingletonではない singletonとして振る舞わない数値である
Rubyの数値(Numeric)のうち、IntegerとFloatの一部はsingletonとして振る舞います。
n = 1
m = 2
n.object_id == (m-1).object_id
# => true
計算しても、同じ値なら同じオブジェクトになります。
0xff.object_id == 255.object_id
# => true
(1.2e-0).object_id == (0.12*10).object_id
# => true
このように、表記が違ったり整数でなかったりしても同じ数値は同じオブジェクトです。処理系全体で同じ数値のオブジェクトは1つしか存在しないんですね。
一方で、コメント欄で @Nabetani さんから指摘があったように、そうでない値もあります。
1e77.object_id == 1e77.object_id
#=> true
1e78.object_id == 1e78.object_id
#=> false
(10**18).object_id == (10**18).object_id
#=> true
(10**19).object_id == (10**19).object_id
#=> false
他にも、こんな値がsingletonではありません。
0.0.object_id == 0.0.object_id
# => true
0.0.next_float
# => 5.0e-324
0.0.next_float.object_id == 0.0.next_float.object_id
# => false
これだけでは、どんな値がsingletonになっているか断定することはできません2。
しかし、少なくともあまりに小さい値や大きな値はsingletonではないことから、目的はキャッシュなのでしょう。
NaNもこちらに該当します。NaNに1足してもNaNですが、
nan.object_id == (nan+1).object_id
# => false
わざわざ定数が用意されていますが、キャッシュはされていないようです。
6. ハッシュのキーになることができるが、値が取り出せなくなることがある
h = {}
h[:a] = 1
h[:a]
# => 1
h[0/0.0] = 2
h[0/0.0]
# => nil
こうなると、普通の方法ではこの「2」は取り出せません。
取り出す方法は色々あると思いますが、一例。
h.to_a
# => [[:a, 1], [NaN, 2]]
h.to_a[1][1]
# => 2
ただし、Float::NAN
をキーに使った場合や変数にNaNを入れた場合はちゃんと取り出すことができます。
h = {}
h[Float::NAN] = 3
h[Float::NAN]
# => 3
n = 0/0.0
h[n] = 4
h[n]
# => 4
ハッシュのキーを比較するときには Float#eql?
メソッドが使われているはず(参考: るりまのObject#eql?)なのですが、前述の通り NaNとの比較は無条件でfalseを返す ことになっている3ので、謎です。
見た感じオブジェクトIDを比較している、つまりBasicObject#equal?
で比較しているのですが、それらしいソースコードは見つけられず……
なお、出典はこちらの記事です。
7. 配列に入れると==
でtrue
を返す
Float::NAN == Float::NAN
# => false
[Float::NAN] == [Float::NAN]
# => true
配列以外のコンテナでも同様の挙動をします。
{ Float::NAN => 0 } == { Float::NAN => 0 }
# => true
こちらも上記同様にBasicObject#equal?
で比較していると思われる怪奇現象です。情報求む。
なお、出典はこちらの記事です。
`Float::NAN` についての重箱の隅 - Qiita
まとめ
NaNの挙動はよくわかりません。
それではみなさん、良いお年を。
バージョン情報
$ ruby -v
ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-darwin19]
参考文献
- NaN - Wikipedia
- 754-2008 - IEEE Standard for Floating-Point Arithmetic - IEEE Standard
- 難読Rubyコードクイズ問題と解説 in RubyKaigi 2019 - エムスリーテックブログ
- instance method Object#eql? (Ruby 2.6.0 リファレンスマニュアル)
- HashのキーをNaNにすると何が起きるか - Qiita
- `Float::NAN` についての重箱の隅 - Qiita