【一人 bugs.ruby Advent Calendar 2020】[Feature #15504] Freeze all Range object【16日目】

一人 bugs.ruby Advent Calendar 2020 16日目の記事になります。

[Feature #15504] Freeze all Range object

リテラルRange を生成すると同じ id のオブジェクトが返ってくるケースがあります。

# これは個別の id が返ってくる
p (1..3).__id__  # => 60
p (1..3).__id__  # => 80
p (1..3).__id__  # => 100

def hoge
  (1..3).__id__
end
# これは同じ id が返ってくる
p hoge    # => 120
p hoge    # => 120
p hoge    # => 120

これの影響で次のようなコードは奇妙な動作になります。

2.times{
  r = (1..3)
  p r.instance_variable_get(:@foo)
  #=> 1回目は nil が返る
  #=> 2回目は :bar が返る
  r.instance_variable_set(:@foo, :bar)
}

これは1回目の呼び出しの r.instance_variable_set(:@foo, :bar) が2回目の呼び出しでも反映されているためです。
他にも Ractor との兼ね合いもあって Range リテラルは暗黙的に freeze されるようになりました。

p (1..3).frozen?  # => true
p (1..3).frozen?  # => true
p (1..3).frozen?  # => true

def hoge
  (1..3).frozen?
end
p hoge    # => true
p hoge    # => true
p hoge    # => true
2.times{
  r = (1..3)
  p r.instance_variable_get(:@foo)
  # error: `instance_variable_set': can't modify frozen Range: 1..3 (FrozenError)
  r.instance_variable_set(:@foo, :bar)
}

これは地味に影響がありそうな気がするんですけどどうなんですかね。今の所既存のコードが壊れた、みたいな報告は見ていませんが…。
これは補足ですが frozen された Range でも次のように中の値が変わってしまうことがあるので注意する必要があります。

r = ('a'..'z').freeze
r.end.upcase!
p r # => "a".."Z"