Ruby 3.1 での each_cons と each_slice の (コーナーケース?) 非互換変更

先日、RuboCop の ruby-head CI が落ちていたので、bugs.ruby-lang.org にフィードバックしていたイシューが以下。

bugs.ruby-lang.org

osyo さんがコメントで教えてくれた PR が以下で、each_conseach_slice の戻り値が nil から self に変わっているというものだった。

github.com

以下は破壊的変更が起きる (単純化した) サンプルです。ブロックで条件によって break を使った戻り値を使うか、each_cons としての戻り値を使うか判定するようなロジックが遭遇したケースです。

Ruby 3.0 以下

% ruby -v
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-darwin19]

% irb
irb(main):001:0> [1, 2, 3].each_cons(2) { |i| break i if true}
=> [1, 2]
irb(main):002:0> [1, 2, 3].each_cons(2) { |i| break i if false}
=> nil

Ruby 3.1

% ruby -v
ruby 3.1.0dev (2021-10-26T00:30:42Z master 7d4c59203f) [x86_64-darwin19]

% irb
irb(main):002:0> [1, 2, 3].each_cons(2) { |i| break i if true}
=> [1, 2]
irb(main):003:0> [1, 2, 3].each_cons(2) { |i| break i if false}
=> [1, 2, 3]

たしかに振る舞いとして戻り値 nil より自然になる気もするが、Ruby 3.1 向けに進んでいる NEWS などで見かけなかったので、影響をどれくらい加味された変更かよく分かっていなかった。

Rails/OSS パッチ会で osyo さんと NEWS にあるといいですねと話していて、osyo さんがパッチを出してくれたのが以下。

github.com

ということで、こういったケースで Ruby 3.1 での非互換変更として影響を受ける可能性があるので、いちおう each_conseach_slice で同様のことを行っていないか見ておくと良いです。

(なお、最初に遭遇した RuboCop の方はそもそものロジックが 🤔 な部分もあったので、Ruby 3.1 互換のロジックに修正済みです。)