Numeric#step の思いがけない挙動
これは何?
ruby の Numeric#step の挙動が思いがけなかったので、調査した。
きっかけ
0.2.step(0.4,0.1).to_a
#=> [0.2, 0.30000000000000004, 0.4]
となる。これは知っている。
0.30000000000000004
とかなるのだるいな、と思い、初期値と増分を有理数にした(成果物は Float でないと困るので、中で to_f している)が、
0.2r.step(0.4,0.1r).map(&:to_f)
#=> [0.2, 0.30000000000000004, 0.4]
となった。え。と思い、試しに to_f
をやめてみると
0.2r.step(0.4,0.1r).to_a
#=> [0.2, 0.30000000000000004, 0.4]
変わらない。始点と増分が 有理数なのに? なんで?
終点も有理数にしてみると
0.2r.step(0.4r,0.1r).to_a
#=> [(1/5), (3/10), (2/5)]
有理数になった。思いがけない挙動
調査
n, m = a.step(b,c).to_a
という感じのことをして、a, b, c の型を変えたら n, m の型がどうなるかを調べた。
a, b, c, n, m の型が全部同じのものは表に含めなかった。
あと、BigDecimal
と Rational
は足せないのでそれも外してある。
a | b | c | n | m |
---|---|---|---|---|
Rational | Integer | Integer | Rational | Rational |
Float | Integer | Integer | Float | Float |
BigDecimal | Integer | Integer | BigDecimal | BigDecimal |
Integer | Rational | Integer | Integer | Integer |
Rational | Rational | Integer | Rational | Rational |
Float | Rational | Integer | Float | Float |
BigDecimal | Rational | Integer | BigDecimal | BigDecimal |
Integer | Float | Integer | Float | Float |
Rational | Float | Integer | Float | Float |
Float | Float | Integer | Float | Float |
BigDecimal | Float | Integer | Float | Float |
Integer | BigDecimal | Integer | Integer | Integer |
Rational | BigDecimal | Integer | Rational | Rational |
Float | BigDecimal | Integer | Float | Float |
BigDecimal | BigDecimal | Integer | BigDecimal | BigDecimal |
Integer | Integer | Rational | Integer | Rational |
Rational | Integer | Rational | Rational | Rational |
Float | Integer | Rational | Float | Float |
BigDecimal | Integer | Rational | BigDecimal | BigDecimal |
Integer | Rational | Rational | Integer | Rational |
Float | Rational | Rational | Float | Float |
BigDecimal | Rational | Rational | BigDecimal | BigDecimal |
Integer | Float | Rational | Float | Float |
Rational | Float | Rational | Float | Float |
Float | Float | Rational | Float | Float |
BigDecimal | Float | Rational | Float | Float |
Integer | BigDecimal | Rational | Integer | Rational |
Rational | BigDecimal | Rational | Rational | Rational |
Float | BigDecimal | Rational | Float | Float |
BigDecimal | BigDecimal | Rational | BigDecimal | BigDecimal |
Integer | Integer | Float | Float | Float |
Rational | Integer | Float | Float | Float |
Float | Integer | Float | Float | Float |
BigDecimal | Integer | Float | Float | Float |
Integer | Rational | Float | Float | Float |
Rational | Rational | Float | Float | Float |
Float | Rational | Float | Float | Float |
BigDecimal | Rational | Float | Float | Float |
Integer | Float | Float | Float | Float |
Rational | Float | Float | Float | Float |
BigDecimal | Float | Float | Float | Float |
Integer | BigDecimal | Float | Float | Float |
Rational | BigDecimal | Float | Float | Float |
Float | BigDecimal | Float | Float | Float |
BigDecimal | BigDecimal | Float | Float | Float |
Integer | Integer | BigDecimal | Integer | BigDecimal |
Float | Integer | BigDecimal | Float | Float |
BigDecimal | Integer | BigDecimal | BigDecimal | BigDecimal |
Integer | Rational | BigDecimal | Integer | BigDecimal |
Float | Rational | BigDecimal | Float | Float |
BigDecimal | Rational | BigDecimal | BigDecimal | BigDecimal |
Integer | Float | BigDecimal | Float | Float |
Rational | Float | BigDecimal | Float | Float |
Float | Float | BigDecimal | Float | Float |
BigDecimal | Float | BigDecimal | Float | Float |
Integer | BigDecimal | BigDecimal | Integer | BigDecimal |
Float | BigDecimal | BigDecimal | Float | Float |
面白いところを抜粋
最初と二番目で型が違う
下記の 2パターンなどは、最初と二番目で型が違う。
1.step(3,1r).to_a
#=> [1, (2/1), (3/1)]
1.step(3r,1r).to_a
#=> [1, (2/1), (3/1)]
他と比べると珍しい感じはするものの、先頭は step
のレシーバそのままで、二番目は step
のレシーバと引数を加算したもの、と思うと自然。
しかし終端が Float
だと
1.step(3.0,1).to_a
#=> [1.0, 2.0, 3.0]
冒頭から Float になる。一貫性は少なめの印象。
終端が Float だからといって Float にされるのは困るような気もする。
Float になると情報が欠落する
s = 1e18
e = s.next_float
p e.to_r-e.floor.to_r
#=> (0/1) なので、e は整数
p s.to_i.step( e, 1).then{ [_1.size, _1.uniq.size] }
#=> [130, 2]
p s.to_i.step( e.to_i, 1).then{ [_1.size, _1.uniq.size] }
#=> [129, 129]
終端が Float になると繰り返し回数が多少増減するのはのは許せる気がする。
終端が Float の場合、ブロック内に 同じ数が何度も来るのはちょっとやだなぁと思う。
ところで無限
ここまでの話とはたぶんあまり関係ないけど、気づいたことをもう一つ。
まずは無限と絡まないパターンを書くと
d=100.0
d.step(d*2,d).take(4)
#=> [100.0, 200.0]
という感じ。普通。
ここで d
を Float::MAX
にすると
d=Float::MAX
d.step(d*2,d).take(4)
#=> [1.7976931348623157e+308, Infinity, Infinity, Infinity]
ということになり、step
のループは終わらなくなる。
d*2
が無限なんだから終わらなくて当然、という視点はありえるけど、無限に到達してるんだから終わってくれよ、と思う。
始点がマイナス無限担ってしまう場合も同様で
d=Float::MAX
(d.to_i*-2).step(d.to_i, d.to_i).take(8).map(&:to_f)
#=> [-Infinity, -1.7976931348623157e+308, 0.0, 1.7976931348623157e+308]
(d.to_i*-2).step(d, d.to_i).take(8).map(&:to_f)
#=> [-Infinity, -Infinity, -Infinity, -Infinity, -Infinity, -Infinity, -Infinity, -Infinity]
無限ループになる。
(d.to_i*-2).step(d, d.to_i)
は、わかりにくい不幸なケースだと思う。
まとめ
ソースコードを見れば謎は解けるんだろうけど、見てない。すいません。
a.step(b,c){ |x| }
の x
の型は、 a
の型、または a+b
の型 になるかと思いきや、そうでもない。
a
, b
, c
の いずれかが Float
だと x
は Float
になる。Float
になると情報が欠落するような状況があるので注意。
それ以外のパターンは(たぶん)x
の型は a
の型、または b
の型になる模様。
あと。
終点が無限だと、無限に到達しても終わらないみたい。
Discussion