🍲

Numeric#step の思いがけない挙動

に公開

これは何?

ruby の Numeric#step の挙動が思いがけなかったので、調査した。

きっかけ

ruby3.4.3-arm64-darwin24
0.2.step(0.4,0.1).to_a
#=> [0.2, 0.30000000000000004, 0.4]

となる。これは知っている。
0.30000000000000004 とかなるのだるいな、と思い、初期値と増分を有理数にした(成果物は Float でないと困るので、中で to_f している)が、

ruby3.4.3-arm64-darwin24
0.2r.step(0.4,0.1r).map(&:to_f)
#=> [0.2, 0.30000000000000004, 0.4]

となった。え。と思い、試しに to_f をやめてみると

ruby3.4.3-arm64-darwin24
0.2r.step(0.4,0.1r).to_a
#=> [0.2, 0.30000000000000004, 0.4]

変わらない。始点と増分が 有理数なのに? なんで?
終点も有理数にしてみると

ruby3.4.3-arm64-darwin24
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 の型が全部同じのものは表に含めなかった。
あと、BigDecimalRational は足せないのでそれも外してある。

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パターンなどは、最初と二番目で型が違う。

ruby3.4.3-arm64-darwin24
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 だと

ruby3.4.3-arm64-darwin24
1.step(3.0,1).to_a
#=> [1.0, 2.0, 3.0]

冒頭から Float になる。一貫性は少なめの印象。

終端が Float だからといって Float にされるのは困るような気もする。

Float になると情報が欠落する

ruby3.4.3-arm64-darwin24
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 の場合、ブロック内に 同じ数が何度も来るのはちょっとやだなぁと思う。

ところで無限

ここまでの話とはたぶんあまり関係ないけど、気づいたことをもう一つ。

まずは無限と絡まないパターンを書くと

ruby3.4.3-arm64-darwin24
d=100.0
d.step(d*2,d).take(4)
#=> [100.0, 200.0]

という感じ。普通。
ここで dFloat::MAX にすると

ruby3.4.3-arm64-darwin24
d=Float::MAX
d.step(d*2,d).take(4)
#=> [1.7976931348623157e+308, Infinity, Infinity, Infinity]

ということになり、step のループは終わらなくなる。

d*2 が無限なんだから終わらなくて当然、という視点はありえるけど、無限に到達してるんだから終わってくれよ、と思う。

始点がマイナス無限担ってしまう場合も同様で

ruby3.4.3-arm64-darwin24
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 だと xFloat になる。Float になると情報が欠落するような状況があるので注意。

それ以外のパターンは(たぶん)x の型は a の型、または b の型になる模様。

あと。
終点が無限だと、無限に到達しても終わらないみたい。

Discussion