ImmutableなセンチネルオブジェクトとコレクションをRactor対応に向けてフリーズ

rails/rails

Rails全体にわたって、センチネル値として使われる Object.new や Set・Hash・Arrayの定数を .freeze するPRがマージされました。#57323 に続くRactor対応の一環であり、Style/MutableConstant Copではカバーできなかったオブジェクト型の定数も含めて不変性が保証されます。

背景

RubyのRactorは、並行処理の安全性を確保するために「Ractorをまたいで共有できるオブジェクトはすべてfreezeされていなければならない」という制約を設けています。Railsをractor-safeにするためには、フレームワーク内で定義されたすべての定数がshareable(共有可能)である必要があります。

直前のPR #57323 では Style/MutableConstant Copを有効化し、リテラル(Array・Hash・文字列)として定義された定数を .freeze する対応が行われました。しかしRuboCopのこのCopは Object.new のようなオブジェクトや、定義後にエントリを追加してからフリーズする Set・Hash、さらに他の定数を組み合わせた演算結果のArrayには作用しません。本PRはその漏れを手動で埋める後続作業です。

技術的な変更

変更は大きく3つのパターンに分類できます。それぞれに対して .freeze を加えることで定数の不変性を明示しています。

パターン1: センチネルオブジェクト(Object.new)のフリーズ

デフォルト値や「未設定」を表すためにフレームワーク内各所で使われる Object.new.freeze が追加されました。対象となったのは以下の定数です:

  • ActionDispatch::Http::Headers::DEFAULT
  • ActionDispatch::Routing::Mapper::POISON
  • ActionDispatch::Routing::Mapper::DEFAULT
  • ActionView::Template::NONE
  • ActiveJob::Exceptions::JITTER_DEFAULT
  • ActiveModel::Attribute::Uninitialized::UNINITIALIZED_ORIGINAL_VALUE
  • ActiveRecord::Attributes::NO_DEFAULT_PROVIDED
  • ActiveSupport::HashWithIndifferentAccess::NOT_GIVEN
  • ActiveSupport::Testing::Assertions::UNTRACKED

これらのセンチネルオブジェクトは equal? による同一性チェック(identity check)で使われており、freezeしても振る舞いは変わりません。

なお activerecord/lib/active_record/attribute_methods/primary_key.rb では、PRIMARY_KEY_NOT_SETBasicObject.new から Object.new.freeze に変更されています。

# 変更前
PRIMARY_KEY_NOT_SET = BasicObject.new

# 変更後
PRIMARY_KEY_NOT_SET = Object.new.freeze

BasicObjectObject のサブセットであり、freeze メソッド自体は BasicObject にも存在しますが、.freeze を呼び出した結果を定数に代入するというパターンに統一しています。

パターン2: 定義後に要素を追加するHash・SetのフリーズをHashの内側まで波及

actionview/lib/action_view/helpers/javascript_helper.rbJS_ESCAPE_MAP は、定義後に2つのUTF-8エントリ(U+2028、U+2029)を別行で追加していたため、rubocop:disable Style/MutableConstant を付けて可変のまま残されていました。本PRではこれらのエントリをHash定義の内側に移動し、キーを .freeze したうえでHash全体を .freeze しています。

# 変更前
JS_ESCAPE_MAP = { # rubocop:disable Style/MutableConstant
  "\\"    => "\\\\",
  # ...
  "$"     => "\\$"
}
JS_ESCAPE_MAP[(+"\342\200\250").force_encoding(Encoding::UTF_8).encode!] = "
"
JS_ESCAPE_MAP[(+"\342\200\251").force_encoding(Encoding::UTF_8).encode!] = "
"

# 変更後
JS_ESCAPE_MAP = {
  "\\"    => "\\\\",
  # ...
  "$"     => "\\$",
  (+"\342\200\250").force_encoding(Encoding::UTF_8).encode!.freeze => "
",
  (+"\342\200\251").force_encoding(Encoding::UTF_8).encode!.freeze => "
"
}.freeze

同様に activesupport/lib/active_support/xml_mini.rbTYPE_NAMES および actionview/lib/action_view/helpers/tag_helper.rbPRE_CONTENT_STRINGS も、定義ブロック末尾で .freeze が呼ばれるよう変更されました。

パターン3: 定数演算結果(Array・Set)のフリーズ

既存のfreezeされた定数から演算によって生成される新しい定数が、freezeされていないまま残っていたケースが修正されました。

# 変更前
VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS

# 変更後
VALUE_METHODS = (MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS).freeze
# 変更前
NORMAL_VALUES = Relation::VALUE_METHODS - Relation::CLAUSE_METHODS -
                [...]

# 変更後
NORMAL_VALUES = (Relation::VALUE_METHODS - Relation::CLAUSE_METHODS -
                [...]).freeze

Rubyでは Array#+Array#- などの演算子は 新しい可変Arrayを返す ため、元の定数がfreezeされていても結果はfreezeされません。Set.new([...]).freeze の形式も同様です。これらの演算結果を明示的にfreezeしています。

activerecord/lib/active_record/attribute_methods/primary_key.rbID_ATTRIBUTE_METHODS では to_set.freeze が追加されています。

# 変更前
ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was id_in_database id_for_database).to_set

# 変更後
ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was id_in_database id_for_database).to_set.freeze

設計判断

センチネルオブジェクトはfreezeしても同一性チェックに影響しないという性質が、今回の変更を安全なものにしています。これらの定数は equal? または obj.equal?(SENTINEL) によってidentityで比較されており、freezeはオブジェクトのidentityを変えないため、既存の利用箇所への影響はありません。

JS_ESCAPE_MAP のリファクタリングは、「定義後に可変オブジェクトとして追記していた」設計を「定義時に完全なHashを構築してからfreezeする」設計に改めるものです。rubocop:disable コメントを除去できたことは、CopによるMutableConstantの静的検出の網羅性が高まったことを意味します。

まとめ

このPRは、RuboCopの Style/MutableConstant Copが自動検出できないセンチネルオブジェクトや演算結果の定数を手動で修正することで、Ractor対応に向けた定数の不変性保証を一歩前進させたものです。各変更は既存の動作を変えず、定数のshareable化という単一の目的に絞られており、Ractor対応に向けた着実な地固めとなっています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
77a02630

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「リード文(総論)→背景・技術詳細(各論)→まとめ(結論)」という3部構成が明確に守られています。各セクションの役割がはっきりしており、非常に読みやすい構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```ruby:ファイルパス)およびPR番号のリンク記法([#123](URL))が、ガイドラインに沿って正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Ractor、センチネルオブジェクト、定数の不変性といったトピックは専門知識を持つエンジニアを対象としており、冗長な初心者向けの説明がなく、ターゲット読者に完全に適合しています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論・各論・結論の構成を持ち、各パラグラフはトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が徹底されています。可読性が非常に高いです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているコードスニペットは、提供されたDiff情報と完全に一致しています。変更前後のコード対比やファイル名の指定も正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「センチネルオブジェクト」「同一性チェック」「Ractor-safe」「shareable」などの技術用語が、文脈に沿って正確かつ適切に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

「Ractorは共有オブジェクトのfreezeを要求する」「Array#+は新しい可変Arrayを返す」といった技術的な説明はすべて正確であり、PRの変更内容と論理的に整合しています。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張([#57323]のフォローアップであること、RuboCopがカバーできない範囲の修正であることなど)は、提供されたPRのDescriptionやDiffによって完全に裏付けられています。ハルシネーションは見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#57323、#57366)やUTF-8文字コード(U+2028, U+2029)などの数値・固有名詞はすべて正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトル「ImmutableなセンチネルオブジェクトとコレクションをRactor対応に向けてフリーズ」は、PRのタイトル「Freeze more immutable constants」の内容と目的を的確に要約しており、完全に一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事はPR情報に厳密に基づいており、LTSやリリース予定など、PRに記載のない外部知識を持ち込んでいません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

このPRを「後続作業」や「一歩前進」と表現しており、時間的な前後関係をPRの文脈に沿って正確に記述しています。