18
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Ruby】NaNのNaNa不思議

Last updated at Posted at 2019-12-18

これは 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

nilfalse以外はすべて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?で比較しているのですが、それらしいソースコードは見つけられず……

なお、出典はこちらの記事です。

HashのキーをNaNにすると何が起きるか - Qiita

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]

参考文献

  1. 整数どうしのゼロ除算0/0や、負数の平方根Math.sqrt(-1)など

  2. 総当たりで調べるには量が多すぎて無理

  3. ruby/numeric.c at master · ruby/ruby

18
6
6

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
18
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?