Rangeの話

[Ruby Advent Calendar 2020](https://qiita.com/advent-calendar/2020/ruby) の 23日目 です。

あまり内部の難しい話はできないのでドキュメントベースで何か書ければいいなと思っていたのですが、ちょうどRubyのバージョンアップでRangeを使った処理の挙動が変わってしまうという事態に遭遇し、それをきっかけに終端なしや始端なしRangeを調べたので、そのことについて書こうと思います。

# 概要

Ruby2.6から終端なしRangeが、Ruby2.7から始端なしRangeが使用できるようになっています。
それぞれのバージョンで生成される始端や終端なしのRangeが面白いので紹介します。

## 終端なしRangeと始端なしRange

```rb
1..  # 終端なし
..10 # 始端なし
```

始端終端共に省略して書くことはできません。

```rb
..  # syntax error
```

ただし、始端終端共になしのRangeは書けます。

```rb
nil..nil
```

### 終端もしくは始端がnilの場合のバージョンごとの差異

これらのバージョンごとの実行結果は以下のようになります。

```rb
1..nil
#  < 2.6: ArgumentError
# >= 2.6: 1..

nil..10
#  < 2.7: ArgumentError
# >= 2.7: ..10

nil..nil
#  < 2.6: nil..nil
# ~> 2.6: nil..
# >= 2.7: nil..nil
```

最初の2つは違いが明白ですが、最後のものはいまいち違いがわかりづらいと思います。

> \< 2.6: nil..nil

これはその通り`nil`から`nil`のRangeです。
sizeは`nil`で`nil`しか含みません。

```rb
range = nil..nil
range.size        #=> nil
range.cover?(nil) #=> true
```

> ~> 2.6: nil..

こちらもそのまま`nil`開始の終端なしRangeです。
こちらもsizeは`nil`で`nil`しか含みません。

```rb
range = nil..nil
range.size        #=> nil
range.cover?(nil) #=> true
```

> \>= 2.7: nil..nil

最後にこちら、最初に述べた通り始端終端共になしのRangeです。
sizeは`Infinity`で**全てを含みます**。

```rb
range = nil..nil
range.size         #=> Infinity
range.cover?(nil)  #=> true
range.cover?(1)    #=> true
range.cover?('s')  #=> true
range.cover?(true) #=> true
```

ちなみに最後の`nil..nil`がsize`Infinity`となるので、`nil..`や`..nil`の場合のsizeも紹介していますが、**`Range#size`は`Array#size`とは違い単純に要素の個数を表すものではありません**。

```rb
('a'..'z').size      #=> nil
('a'..'z').to_a.size #=> 26
```

要素数を表すメソッドではありますが、終端始端共に`Numeric`のサブクラスオブジェクト、もしくは`nil`の場合以外は`nil`を返します。

### Rangeは破壊的変更が不可能

> Ruby の Range クラスは immutable です。つまり、オブジェクト自体を破壊的に変更することはできません。ですので、一度生成された Range のオブジェクトの指し示す範囲は決して変更することはできません。

そう言われるとなんとかして破壊的変更を行いたくなりますね。
...しかし、色々調べてみましたが破壊的な変更を行う手段はありませんでした。

### るりまの話

`Range#begin`と`Range#first`、`Range#end`と`Range#last`が同じ説明として書かれていますが、始端なしや終端なしの場合結果が異なります。

[Range#begin](https://docs.ruby-lang.org/ja/latest/method/Range/i/begin.html)

```rb
(..10).begin #=> nil
(..10).first #=> RangeError
```

[Range#end](https://docs.ruby-lang.org/ja/latest/method/Range/i/end.html)

```rb
(1..).end #=> nil
(1..).last #=> RangeError
```

ソースコード読んでみると若干実装が違うみたいですね。

```c
# Range#begin
static VALUE
range_begin(VALUE range)
{
    return RANGE_BEG(range);
}
```

```c
# Range#first
static VALUE
range_first(int argc, VALUE *argv, VALUE range)
{
    VALUE n, ary[2];

    if (NIL_P(RANGE_BEG(range))) {
        rb_raise(rb_eRangeError, "cannot get the first element of beginless range");
    }
    if (argc == 0) return RANGE_BEG(range);

    rb_scan_args(argc, argv, "1", &n);
    ary[0] = n;
    ary[1] = rb_ary_new2(NUM2LONG(n));
    rb_block_call(range, idEach, 0, 0, first_i, (VALUE)ary);

    return ary[1];
}
```

C言語は全く読めないのですが、パッとみた感じ`Range#first`や`Range#last`は`begin`や`end`で取得した値が`nil`だった場合に例外を発生させるようです。
それ以降は`Range#first`や`Range#last`は引数を渡せば最初や最後を起点に値を複数取得できるのでその部分の処理みたいです。

この辺りの動作の違いを上手に説明できる文章を考えついたら修正PRを作成します。

ちなみにるりまの修正はここの「edit」リンクからPRの作成をすることで行えます。
![スクリーンショット 2020-12-21 23.07.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262029/c7c81f0d-a923-966b-72a7-03dd31735e66.png)

`Range#size`も終端もしくは始端が`Numeric`のサブクラスオブジェクト以外の場合は`nil`を返すとありますが、`1..nil`の終端`nil`は`Numeric`のサブクラスオブジェクトではありませんが`nil`ではなく`Infinity`が返りますね。ここは解釈がどうなのかわからない[^1]ので単純に誤りとは言えなさそうですが、表現をわかりやすく変更する余地はありそうです。

[Range#size](https://docs.ruby-lang.org/ja/latest/method/Range/i/size.html)

# まとめ

普段よく使うようなクラスのオブジェクトもドキュメントを改めて読みながら手元で動かしていると新しい発見があったりして楽しいです。

最初に述べた「RubyのバージョンアップでRangeを使った処理の挙動が変わってしまうという事態」についてですが、以下のようなコードでした。

```rb
(time_or_nil..).cover?(time)
```
Timeオブジェクトの値を上記のような終端なしRangeと`===`で比較し、含まれていれば`true`を返すような処理でした。Rangeは`nil..`のような形を取る場合があり、この場合必ず`false`を返していたのですが、ここが2.7に上げたタイミングで必ず`true`を返すようになっていました。もしかすると同じような事態に遭遇する方もいらっしゃるかもしれませんのでご注意ください。

プライベートならまだ大丈夫ですが、仕事とかだと他人が書いたコードは全てを把握してませんので、こういうバージョンアップで挙動が変わる箇所は特に見落としやすいです。きちんと挙動が変わった時に拾えるよう、日頃からテストはきっちりと書いておきたいですね。

# 参考資料
[るりま](https://docs.ruby-lang.org/ja/)

[^1]: `(1..nil).end`の値`nil`を終端と捉えても良いのかどうか。ここでの`nil`は「終端なし」を表現するので終端は`nil`ではなく「なし」だとも言える。