CRubyハッシュ実装 st.cのソースコードを読む

st.c のソースコードを読もうと思った経緯

CRuby Quest を読んでいて以下の説明を見かけました。

st_table はオープンアドレッシングという方法で衝突を回避します。 ... st_tabel ではオープンアドレッシングを採用することによって bins がシンプルな配列で実現することが出来ています。

ちょうど前回のブログ記事でコンピューターサイエンス基礎のデータ構造&アルゴリズムあたりに取り組んでいました。

kozy4324.hatenablog.jp

「ハッシュ」「オープンアドレッシング」あたりのキーワードが自分の中でホットだったのと、理論だけではなく実装にも触れるいい機会なのではないかということで、興味の赴くままに CRuby のソースコードを読むことにしました。

ちなみにこのブログ記事は 柏.rb #26 もくもく会の時間を使って文章を書いています。

kashiwarb.connpass.com

ハッシュの基礎知識

概要の説明は wikipedia に委ねます。

ハッシュテーブル - Wikipedia

ハッシュテーブル (英: hash table) は、キーと値の組(エントリと呼ぶ)を複数個格納し、キーに対応する値をすばやく参照するためのデータ構造。

Rubyで言うと組み込みライブラリの Hashクラス がそれで、ハッシュ式 が言語仕様に組み込まれていますね。

{ a:"A", b:"B", c:"C" }

:a , :b , :c がそれぞれ「キー」、"A" , "B" , "C" がそれぞれ「値」、キー :a に紐づいている値が "A" でそのペアが「エントリ」と言うことですね。

そんなハッシュも内部の基本的なデータ構造は配列です。キーを元に生成された「ハッシュ値」を添字にして値を管理します。イメージは以下の通り。

hash(a) = 2
hash(b) = 5
hash(c) = 8

  +---+
0 |   |
  +---+
1 |   |
  +---+
2 |"A"|
  +---+
3 |   |
  +---+
4 |   |
  +---+
5 |"B"|
  +---+
6 |   |
  +---+
7 |   |
  +---+
8 |"C"|
  +---+
9 |   |
  +---+

このキーからハッシュ値を生成する関数(上記だと hash(x))が「ハッシュ関数」というものです。最もシンプルなハッシュ関数だと値の整数値を配列サイズで除算した余り(剰余、mod )をハッシュ値とする「除算法」があったりします。

さて、一般的にキーが取りうる値範囲よりも内部の配列サイズの方が小さいことが一般的です。なので異なるキーから同じハッシュ値になることももちろんあります。それが「衝突」です。

hash(d) = 2

  +---+
0 |   |
  +---+
1 |   |
  +---+
2 |"A"|
  +---+
3 |   |
  +---+
4 |   |
  +---+
5 |"B"|
  +---+
6 |   |
  +---+
7 |   |
  +---+
8 |"C"|
  +---+
9 |   |
  +---+

"D"はどこに格納する???

この衝突を解決するための方法がいくつかあって、その一つが「オープンアドレッシング」ということになります。

いくつかの衝突解決手法

  • 直接連鎖法
    • 分離連鎖法
  • オープンアドレス法
    • 線形探針法 (Linear Probing)
    • 2乗探針法 (Quadratic Probing)
    • 二重ハッシュ法 (Double Hashing)

ここで言いたいことはCRubyはオープンアドレス法を使っているということ。

いくつかのハッシュ関数

キーワードだけ列挙。

  1. 除算法
  2. 乗算法
  3. 多項式ローリングハッシュ
  4. FNV Hash
  5. Murmur Hash
  6. xxHash
  7. SipHash
    • 意図的にハッシュの衝突を起こすようなDoS攻撃に対する手法

CRubyはSipHashを採用しているらしい。

やっとソースコードを読む

hash.c という「いかにも」というソースファイルを見つけたのでそれから読み始めます。

https://github.com/ruby/ruby/blob/v4.0.0/hash.c

CRubyの予備知識がないので1mmも分からないのですが、53行目〜68行目のコメントを読みます。

/* Flags of RHash
 *
 * 1:     RHASH_PASS_AS_KEYWORDS
 *            The hash is flagged as Ruby 2 keywords hash.
 * 2:     RHASH_PROC_DEFAULT
 *            The hash has a default proc (rather than a default value).
 * 3:     RHASH_ST_TABLE_FLAG
 *            The hash uses a ST table (rather than an AR table).
 * 4-7:   RHASH_AR_TABLE_SIZE_MASK
 *            The size of the AR table.
 * 8-11:  RHASH_AR_TABLE_BOUND_MASK
 *            The bounds of the AR table.
 * 13-19: RHASH_LEV_MASK
 *            The iterational level of the hash. Used to prevent modifications
 *            to the hash during iteration.
 */

ST_TABLE と AR_TABLE という語彙が気になりますね。さっそくLLMに聞いてしまいましょう。

Q. crubyのhash実装みてたらst_tableとar_tableというものが出てきたけど、これらは何?
- ar_table (Array Representation): 小さいHash向けの軽量実装
- st_table (Symbol Tableのstが由来): 通常のハッシュテーブル実装

st_table は st.c に実装されている、CRuby全体で使われる汎用ハッシュテーブルです。

なるほど st.c !

https://github.com/ruby/ruby/blob/v4.0.0/st.c

ヘッダーのコメントに内部実装に関する説明が書かれてそうですね。

/* The original package implemented classic bucket-based hash tables
   with entries doubly linked for an access by their insertion order.
   To decrease pointer chasing and as a consequence to improve a data
   locality the current implementation is based on storing entries in
   an array and using hash tables with open addressing.  The current
   entries are more compact in comparison with the original ones and
   this also improves the data locality.

   The hash table has two arrays called *bins* and *entries*.

     bins:
    -------
   |       |                  entries array:
   |-------|            --------------------------------
   | index |           |      | entry:  |        |      |
   |-------|           |      |         |        |      |
   | ...   |           | ...  | hash    |  ...   | ...  |
   |-------|           |      | key     |        |      |
   | empty |           |      | record  |        |      |
   |-------|            --------------------------------
   | ...   |                   ^                  ^
   |-------|                   |_ entries start   |_ entries bound
   |deleted|
    -------

挿入順を保持する(Hashのドキュメントにも「ハッシュに含まれる要素の順序が保持される」とありますしね)ためと、最適化のため、現在の内部データ構造としては「配列」と「オープンアドレッシングのハッシュ表」の2つがベースになっていると言及がありますね。アスキーアートによる説明も分かりやすくてイメージをつけやすいですね。

柏.rb のもくもくタイムの作業としてはここまでで時間切れになってしまいました。ブログ記事としては一旦ここで区切ります。

Udemyの「Computer Science 101」をコンプリートした備忘録

所属組織の福利厚生でUdemyし放題なのでコンピューターサイエンスの講座を最近受講していた。

www.udemy.com

プライベートの隙間ヒマ時間で毎日1〜2レクチャー見る感じで進めてたら2ヶ月半ほどかかった。つい先日ちゃんと最後まで完走(コンプリート)できたので備忘録を残しておく。

なぜ今更コンピューターサイエンス基礎を?

これは前回のKashiwa.rbでも触れたのだけど、

Lightning近況報告 - Speaker Deck

  • CSを修めることになんとなくの憧れがある
  • CSで具体的に何を学ぶのか、また学んだことで実務にどう寄与できるのかが人に説明できるレベルで理解できていない

ということで、機会があればちゃんと取り組んでみたいなと長年考えていたものだった。いい機会だし取り組んでみるかという流れ。

Udemyの「Computer Science 101」で学べること

基本的なデータ構造とアルゴリズム、そして計算量についての基礎的な内容を順番に説明していくというもの。計算理論の初歩中の初歩という内容だったように思える。

最初に貼ったリンクで確認できる「コースの内容」も以下の通り。

  • Introduction
  • Analyzing Algorithms
  • Arrays
  • Linked Lists
  • Stacks and Queues
  • Sorting Algorithms
  • Trees
  • Heaps
  • Graphs
  • Hasing

一番ボリュームがあったセクションは「Sorting Algorithms」で、バブルソート、選択ソート、挿入ソート、クイックソート、マージソートのアルゴリズムと計算量を順番に説明していくという王道中の王道といった内容だった。

実務に役立つ内容だったか否か

実務といってもソフトウェアエンジニアリングの対象は多岐にわたり一概には言えないところではある。なので自分が主に携わるところの「Webサービスのアプリケーションエンジニア、特にRailsなどのフレームワーク上で業務アプリケーションを実装する業務」というところで考えてみる。

具体なソートのアルゴリズムや計算量をちゃんと理解することは別に実務に直接役立つものでもないなぁという感想。それはそうで業務でソートアルゴリズムを自分で実装することなどこれまでなかったし、この先もないでしょうねという。

またちょうどXの誰かのツイートで見かけた内容を借用させてもらうのだけど、講座で説明されていた内容の個人的見解としては「実務の9割の場面で特段役立つものでもないし、残り1割の場面で知っていれば考え方の手がかりぐらいにはなりそうだけど別に知らなくてもなんとかなるのでは」ぐらいの感じ。昨今のAIコーディング全盛の時代であれば1割どころかより頻度が下がりそうですらある。

とはいえ抽象なところでの計算量という概念やその評価の仕方を理解しておくことはWebサービスの開発に携わる上でもやはり必要だよなぁということも改めて思った。

ということで同僚(自分と似たような実務に携わっている人)に対して答えるならば「直接実務に役立つ内容とは言えないけど、計算量あたりの概念を押さえておくにはいいんじゃない?とは言えそれ学ぶのであればもっといいコンテンツありそうだから、時間があり余っている限りは特にオススメはしないかな」といった感じになりそう。全然オススメしてなくて自分で書いてて笑ってしまった。そんな感じです。

Webアプリケーション開発で「計算量」という概念を押さえておくことの話

計算量という概念をちゃんと押さえておくことは自分のこれまでの実務経験で活きてきた感覚はある。もしかしたらここがこの記事の本題かもしれない。

例えばあるエンドポイントがあって、日々の運用の中でレイテンシを監視しているとする。ある特定の顧客(マルチテナンシーでいうところのテナント)でだけレイテンシが悪化している場合にどうアプローチしていく?を考えてみる。

このケースで自分がまず考えるのは、レイテンシに対して支配的になっているデータセットが存在しないか?ということと、そのデータセットに対しての計算量がどうなっているのか、という2点。O(n) なのか O(n log n) 、または O(n2) や O(mn) という機序でレイテンシに寄与しているデータセットが存在しないかというアプローチで、これは計算量という話を一定理解していないと出てこない発想かなと考えている。

そしてこの場合、プログラム実装を上から順番に見る必要はなくて遅いレイテンシのテナントと速いレイテンシのテナントのデータセットを見比べて有意な差があるかをチェックし、もしそういったデータが見つかればそれを処理している実装箇所やDBクエリを重点的に確認することでレイテンシが遅くなる機序を割とすぐ解明できてきたイメージがある。

そういえばレイテンシが遅くなっている原因がDBクエリだった場合、JOINしているクエリでインデックスがうまく使えていない場合に O(mn) のような事象になっていたのは過去に何度か見かけたなということも思い出した(細かいところを言うとここら辺はSQLの実行計画によるところではある)。Railsアプリケーションだと割とみんな N+1 問題には気をつけている印象はあるけど、サービスが続く中でデータが増加し顕在化する O(mn) な事象なんかも計算量という切り口で認識してアプローチできるといいのではという話でした。

Udemyの「Computer Science 101」固有の話(愚痴)

  • 日本語字幕はなかった
  • iPhoneで見ていたがアプリだと字幕が選べなかった
  • アプリではなくPWAで英語字幕で見ていたのだけど動画ごとの初期字幕と言語並び順がいつもバラバラだった
  • アメリカの大学教授によるコンテンツだと思うのだけど割と早口で分かりやすい英語ではなかった印象(これはもちろん自分の英語力に起因する話ではある)

という感じで、英語の耳ならしをしたいというモチベーションがない限りは上記点でもオススメはしないなぁという感想を持った。

まとめ

  • Udemyでコンピューターサイエンス基礎的な講座を受講して個人的には面白かったのだけど、ぶっちゃけオススメはしないなぁという感想でした
  • 計算量の話は認識していて損はないけど、AIコーディング全盛の昨今だとその価値はより薄れているかもしれないなって思った
  • コンピューターサイエンスは引き続き取り組んでいきたいのでオススメコンテンツがあれば教えてください!

2026年3月・4月のふりかえり

お久しぶりですkozy4324です。毎月書いていた「ふりかえり」記事も1ヶ月すっ飛ばす程度にバタバタと暮らしていました。2ヶ月分まとめます。

前回はこれ:

kozy4324.hatenablog.jp

3月4月の主な出来事

子1の小学校卒業式&4月から始まる新生活の準備、子2の進級、所属のあれこれ(転職はしていないけど見た目の所属は4月から変わっております)、などなど。生活のペースが4月から変わったなぁと書いてて思った。

朝型にシフトした

4月から家族全員が6:00起床するスタイルになった。おかげで22:00〜23:00ぐらいには眠くてたまらない。自分は割とよく寝れる体質っぽく入眠もスムーズなので辛くはない。

朝活でランニング復活した

朝、子×2の送り出しを終えると2時間ぐらいフリータイムが発生する。ここでランニングと筋トレと英語学習をルーティンでこなすようにした。

4月に入ってから雨も降らず距離を稼げている。ペースと体重は気にしない感じでユルユル走っている。

英語学習の比重を上げた

相変わらずSpeakは続けているとして、L&RだけではなくW&Sも何とかしたいなぁと思うようになった。4月から朝活にうまく取り込めていて、今取り組んでいるのは以下な感じ。

  • Listening: Speak、TOEICテキスト、NHKゴガク、英語系Podcast
  • Reading: 英語の技術記事読む、TOEICテキスト
  • Writing: 英作文ドリル
  • Speaking: Speak

Speakingが弱いなぁ、が、これでしばらく継続してみたい。TOEICテキストを一通りやったら一度TOEIC L&Rも受けたいなと考えている。スコア800オーバーが目標。

OSS活動

GitHubの草を生やすペースを落としている。英語学習に比重を置いたのと、「とりあえず何でもいいからコミット作って草生やす」ってのを辞めてみた。RubyKaigi 2026(には行けていないのだけど)を経てRuby方面エコシステムのアップデートがたくさんあるはずなのでそこらへんの情報を追いかけるところからやっていきたい。

実のところ、OSS活動をまだ日々のルーティンにうまく組み込めていないという話もある。

勉強会

Kashiwa.rbは4月もやったぞ!

Kashiwa.rb以外の地域Ruby勉強会にも積極的に足を運びたいと思ってます。ここはやっていき。

野球観戦

実に2年ぶりにZOZOマリンスタジアムに行ってきました。子×2も大きくなったので実に10年以上ぶりの外野観戦。そしてアイコン写真から8年経過していることに気づいたので妻に同じ絵面で写真を撮ってもらった。

アイコン画像を最新にしようかなって思ったのだけど、顔全然変わってないので別にいいやとなった。ちなみに体重は8年前とほぼ変わっていない(コロナ禍に太ったけど元に戻したのです)

2026年度の目標

健康であることかな。一定の出力を保てるように毎月頑張りすぎずに頑張りたい。

2026年2月のふりかえり

そういえば書いていなかったことを思い出したので書いておく。

前回はこれ:

kozy4324.hatenablog.jp

OSS活動

rbs-siggenというのを開発している。READMEはまだちゃんと書いていない。このネタで記事1本書こうと思っている。

付随してデバッグ用途でgemを2つほど開発した。

基本的にREADMEの記述はAIに丸投げしている。traceologistについては100%バイブコーディングで作られている。ユーザーが自分だけの小規模gemぐらいならバイブコーディングも悪くないなって思った。

英語学習

Speak & NHKゴガクの2本立てで継続している。

読書

2月はコードばっか書いていてマジで読書していない。呪術廻戦モジュロ②は読んだ。

ランニング

復活!というほど距離は稼げていない。花粉つらいよ花粉。しっかり距離を走っていこう。

ブログ

アウトプット少ないね。意識していこう。

勉強会

3ヶ月ぶりにKashiwa.rbに参加した。3月もやるぞ!

KPT

  • Keep
    • OSS活動
    • 英語学習
    • ランニング
    • 勉強会
  • Problem
    • 読書
    • ブログ
  • Try
    • アウトプットするぞ

objenealogistというgemを作った

github.com

発音と命名の由来

Object(オブジェクト) + Genealogist(ジーニーアーロジスト) -> オブジーニーアーロジスト

という想定なのだけど、本当にそう発音していいかは知らない。造語です。

Genealogistは「家系、血縁関係、家系図を専門的に調査・研究する専門家」ということで、家系図=クラス継承ツリーを出力するということでこういう命名をしました。

使い方

例えばMyClassクラス があったとしてClassクラスに #to_tree メソッドが生えているのでそれを呼びます。

puts MyClass.to_tree

以下のような出力結果が得られます。

C MyClass (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:39)
│ ├ c (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:43)
│ └ singleton_c (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:44)
│
├── M M2 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:7)
│ └ m2 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:8)
│
├── M M1 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:3)
│ └ m1 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:4)
│
└── C NS::C2 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:32)
    │ └ c2 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:35)
    │
    ├── M M4 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:15)
    │ └ m4 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:18)
    │
    ├── M M3 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:11)
    │ └ m3 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:12)
    │
    └── C C1 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:25)
        │ └ c1 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:28)
        │
        ├── M M5 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:21)
        │ └ m5 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:22)
        │
        └── C Object (location: /Users/kozy4324/.rbenv/versions/4.0.0/lib/ruby/gems/4.0.0/gems/pp-0.6.3/lib/pp.rb:700)
            ├── M PP::ObjectMixin (location: /Users/kozy4324/.rbenv/versions/4.0.0/lib/ruby/gems/4.0.0/gems/pp-0.6.3/lib/pp.rb:348)
            │ ├ pretty_print (location: /Users/kozy4324/.rbenv/versions/4.0.0/lib/ruby/gems/4.0.0/gems/pp-0.6.3/lib/pp.rb:362)
            │ ├ pretty_print_cycle (location: /Users/kozy4324/.rbenv/versions/4.0.0/lib/ruby/gems/4.0.0/gems/pp-0.6.3/lib/pp.rb:379)
            │ ├ pretty_print_inspect (location: /Users/kozy4324/.rbenv/versions/4.0.0/lib/ruby/gems/4.0.0/gems/pp-0.6.3/lib/pp.rb:402)
            │ └ pretty_print_instance_variables (location: /Users/kozy4324/.rbenv/versions/4.0.0/lib/ruby/gems/4.0.0/gems/pp-0.6.3/lib/pp.rb:390)
            │
            ├── M Kernel (location: /Users/kozy4324/.rbenv/versions/4.0.0/lib/ruby/gems/4.0.0/gems/pp-0.6.3/lib/pp.rb:720)
            └── C BasicObject

クラス定義とメソッド定義のsource locationも出力されるのでVSCodeであれば command + click でコードジャンプできるので便利ですね。

ブログ記事上だとlocationあると見づらいのでシンプルなバージョンも貼っておきます。

> puts MyClass.to_tree(show_locations: false)
C MyClass
│ ├ c
│ └ singleton_c
│
├── M M2
│ └ m2
│
├── M M1
│ └ m1
│
└── C NS::C2
    │ └ c2
    │
    ├── M M4
    │ └ m4
    │
    ├── M M3
    │ └ m3
    │
    └── C C1
        │ └ c1
        │
        ├── M M5
        │ └ m5
        │
        └── C Object
            ├── M PP::ObjectMixin
            │ ├ pretty_print
            │ ├ pretty_print_cycle
            │ ├ pretty_print_inspect
            │ └ pretty_print_instance_variables
            │
            ├── M Kernel
            └── C BasicObject

作ったモチベーション

RailsのActiveRecordモデルのメタプログラミング事情を調べたくて作りました。例えばDBカラムに title というカラムがあった場合に #title, #title=, #title? といったメソッドがメタプログラミングによって生えるのですが、それらがクラス継承ツリー上はどこで定義されることになるメソッドなのか、また同様にメタプログラミングで生えるメソッドは他に何があるのかあたりを確認したいなとなりました。

Rubyは動的定義されたメソッドも動的に取得できる Object#methods や Module#instance_methods あたりが使えるので便利ですよね。

実際に出力した内容はgistに置きました。動的に定義されるDBカラムメソッド群は Article::GeneratedAttributeMethods に定義されているのが分かると思います(このmoduleもまた動的に定義されたmoduleです)。

https://gist.github.com/kozy4324/2e1e3c565f12329007922f2ec4092da8

実装を一工夫したところ

メソッド定義位置は Object#method で取得できるMethodオブジェクトの#source_locationから取得可能ですが、moduleやclassの定義位置の取得はAPIには見当たりませんでした。なのでメソッド定義位置から取得できるファイルパスでソースファイルを読み込み→Prismパース→ASTレベルで走査という順番でやってみました。ASTをシュッと利用できるRubyistってカッコいいですよね。私もそんなRubyistになりたい。

  class ClassVisitor < Prism::Visitor
    attr_reader :found

    def initialize(target_class_names)
      @target_class_names = target_class_names.map(&:to_s).map(&:to_sym)
      @found = []
      @stack = []
      super()
    end

    def visit_class_node(node)
      @stack << node.name
      name = @stack.join("::").to_sym
      @found << [name, node.location] if @target_class_names.include?(name)
      super
      @stack.pop
    end

    alias visit_module_node visit_class_node
  end
        source = File.read(path)
        visitor = ClassVisitor.new(clazz.ancestors)
        Prism.parse(source).value.accept(visitor)
        visitor.found.each do |name, def_location|
          (location_map[name] ||= []) << [path, def_location]
        end

1歩進んだ使い方: 出力結果をLLMに渡す

irbやRails console上で使うことを想定して作りました。ActiveRecordモデルの継承やメソッド数になるとコピペもままならないので、お手軽にファイル出力できるようにもしておきました。

Article.to_tree >> "article_tree.txt"

この出力結果をLLMにぶん投げればいいわけですね。

$ cat article_tree.txt | claude "Articleモデルのクラスツリーで、これをベースに要点だけを絞ってクラス図を書いて"

みたいな感じで出力してもらった内容が以下の通り。悪くない。

Article Model Class Diagram
===========================

┌─────────────────────────────────────────────────────────────┐
│                         Article                              │
├─────────────────────────────────────────────────────────────┤
│ Attributes:                                                  │
│   id, title, body, user_id, created_at, updated_at          │
├─────────────────────────────────────────────────────────────┤
│ + GeneratedAssociationMethods                                │
│ + GeneratedAttributeMethods                                  │
└───────────────────────────┬─────────────────────────────────┘
                            │ extends
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                    ApplicationRecord                         │
└───────────────────────────┬─────────────────────────────────┘
                            │ extends
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                    ActiveRecord::Base                        │
├─────────────────────────────────────────────────────────────┤
│ 主要な include モジュール:                                    │
│                                                              │
│ [永続化・CRUD]                                               │
│   ├─ Persistence      (save, destroy, update, reload...)    │
│   ├─ Callbacks        (before_save, after_create...)        │
│   └─ Transactions     (transaction, with_lock...)           │
│                                                              │
│ [バリデーション]                                              │
│   ├─ Validations      (valid?, validate!, errors)           │
│   └─ ActiveModel::Validations                                │
│                                                              │
│ [属性・アクセス]                                              │
│   ├─ AttributeMethods ([], []=, attributes, has_attribute?) │
│   │   ├─ Read/Write   (read_attribute, write_attribute)     │
│   │   ├─ Dirty        (changed?, saved_changes?)            │
│   │   └─ PrimaryKey   (id, id=, to_key)                     │
│   └─ Attributes                                              │
│                                                              │
│ [関連付け]                                                    │
│   ├─ Associations     (association, belongs_to, has_many)   │
│   ├─ AutosaveAssociation                                     │
│   └─ NestedAttributes                                        │
│                                                              │
│ [クエリ・スコープ]                                            │
│   ├─ Scoping          (default_scope, scope)                │
│   └─ Inheritance      (STI support)                          │
│                                                              │
│ [ロック]                                                      │
│   ├─ Locking::Optimistic  (楽観的ロック)                     │
│   └─ Locking::Pessimistic (lock!, with_lock)                │
│                                                              │
│ [その他]                                                      │
│   ├─ Turbo::Broadcastable (broadcast_append, refresh...)   │
│   ├─ ActiveStorage::Attached::Model (attachment_changes)    │
│   ├─ GlobalID::Identification (to_gid, to_sgid)             │
│   ├─ Integration      (cache_key, to_param)                 │
│   └─ Serialization    (to_json, as_json)                    │
└───────────────────────────┬─────────────────────────────────┘
                            │ extends
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                         Object                               │
│  (ActiveSupport拡張: blank?, present?, try, in?, etc.)      │
└─────────────────────────────────────────────────────────────┘


主要なメソッドの由来まとめ
==========================

| 機能                       | モジュール                    |
|----------------------------|------------------------------|
| save, destroy, update      | Persistence                  |
| valid?, errors             | Validations                  |
| title, body, user_id       | GeneratedAttributeMethods    |
| changed?, saved_changes?   | AttributeMethods::Dirty      |
| before_save, after_create  | Callbacks                    |
| transaction, with_lock     | Transactions                 |
| broadcast_append           | Turbo::Broadcastable         |
| to_gid, to_sgid            | GlobalID::Identification     |

まとめ

Railsのソースコードリーディングや内部実装を探るお供にobjenealogistというgemを作ってみました。LLMと連携する前提でこういったツール作るのも悪くないです。

2026年1月のふりかえり

1月〜2月頭まで家庭のいろいろ(子1の受験本番、子2の習い事発表会、etc...)があり多忙だった。ゆえにいつものこの「ふりかえり」も遅れての更新。

前回はこれ:

kozy4324.hatenablog.jp

OSS活動

忙しいなりにも続けている。

前回はherb/reactionviewを触っていたけど、投げたPRがスルーされると「まぁいいか」という感じで別のことをやっていた。

Rubyの型関連、RBSあたりを触っていた。gem_rbs_collection に趣味でrbs追加してて、差分管理するアイデアが思いついたのでgemを一つ作成した。

GitHub - kozy4324/rbs-patch

おそらく需要がないのでアイデアを形にしたところで一定満足したのだけど、rbsのASTとか理解できたのでそれはそれで良かった。

rbs関連で別アイデアがあって、Rubyの動的なメソッド定義に対して、sigも動的に生成できればいいのではって思ってrbs-siggenというgemを作り始めた。まだREADMEもちゃんと書いていないのでここで詳細を書いてみる。

class A
  def self.scope(name)
    define_method("generated_#{name}") do
      puts "hello generated #{name}"
    end
  end

  scope :hoge
end

というクラスがあれば、

class A
  def self.scope: (Symbol name) -> void
end

というsigがベースにあって、動的に生成されたメソッドに対しては以下のsigがあれば良い。

class A
  def generated_hoge: () -> void
end

rbsのannotationsにそういうsigのテンプレートがあればいけるんちゃうか?というのを思いついた。

class A
  %a{siggen:
    def generated_<%= name %>: () -> void
  }
  def self.scope: (Symbol name) -> void
end

これぐらいならASTを走査すればできる。この方針でどんなことができるのかを探ってみたいと思って手を動かしている。

英語学習

Speakばっかりやっている。あとNHKゴガク。ここは継続。

読書

久しぶりに技術書じゃない書籍を読了した。

企業変革のジレンマ 「構造的無能化」はなぜ起きるのか https://www.amazon.co.jp/dp/4296115928

チームの能力は単なる個人の能力の総和にはならないって話はよくあって、総和以上にもなれば以下にもなる。この本では企業組織においてなぜ総和以下になるのかという話がなされており、過去に自分が所属した組織とも重なる部分が多々あって面白く(?)読ませてもらった。チームとか組織に関する話は割と好きな方なので、これ系の書籍はまた興味あるやつを発見すれば読んでいきたい。

ランニング

いろいろがあり全然走れなかった。体調に気をつけながら巻き返していきたい。

ブログ

全然更新してなかった。小ネタは日々あるのでちゃんとアウトプットしていこう。

勉強会

12月〜1月は自粛してたので参加はゼロ。2月からやっていきです。

というわけで今月末2/27(金)に主催するKashiwa.rbをやります!参加するのは3ヶ月ぶりです。

kashiwarb.connpass.com

KPT

  • Keep
    • OSS活動
    • 英語学習
    • 読書
  • Problem
    • ブログ
  • Try
    • ランニング
      • まずはペースを戻すところから!
    • ブログ
      • ちゃんと書く!
    • 勉強会
      • やっていくぞ!

OSS活動記 #6 - rbs_rails - prismパーサーを利用するようにする

対象リポジトリ

github.com

作成したPR

github.com

経緯

rbs_railsはRailsアプリケーションでのrbs(Rubyの型定義ファイル)作成を支援してくれるもの。ActiveRecordモデルはDBカラムから動的にメソッド群が生える。そういった動的に生えるメソッドをケアしてくれるのがrbs_rails。

RailsアプリでのSteep利用を改めて整理しようと思ってrbs_railsを実行したら以下のwarningが出ていた。

warning: parser/current is loading parser/ruby33, which recognizes 3.3.x-compliant syntax, but you are running 4.0.0.
Please see https://github.com/whitequark/parser#compatibility-with-ruby-mri.

これrubocop-astで見たことあるやつだ!となったのでrbs_railsでもparser使っていることがすぐ検知できた。Ruby 4.0.0を使っているのだが、Ruby 3.4でもなくRuby 3.3系でパースされるのは心象良くないなと思ったのでコードを覗いてみた。

rbs_railsでparser使っている実装

以下でASTを取得しているのに利用している。

https://github.com/pocke/rbs_rails/blob/f1a7764617f57aac96e73d81fba1fedf5929bf0d/lib/rbs_rails/active_record.rb#L474

ASTからいくつかのメソッド呼び出しパターンを検知してrbsを追加する実装になっているようだ。そこらへんの詳細は主題から外れるのでこんなところでよし。

Parser::CurrentRuby とは

「parser」と書くと曖昧さがあるので「whitequark/parser」とする。そのgemの中に、実行時のRubyバージョンをチェックしてバージョン毎のパーサークラスを振り分けてくれるものが parser::CurrentRuby 。

https://github.com/whitequark/parser/blob/9520c3ac88f808595eea8f517c2eb271867f9a61/lib/parser/current.rb

冒頭のwarningsは warn_syntax_deviation メソッドで定義されているものだということもこの実装から確認できる。

PrismのTranslationレイヤー

Prismパーサーは既存パーサーとの互換性を持っておりI/FはそのままでPrismパーサーへと移行可能になっている。ここら辺の実装を把握&参考にさせてもらったのはrubocop-astでの実装なのでそのリンクを貼っておく。

https://github.com/rubocop/rubocop-ast/blob/69036498c11ca944c6099d1b672ba408f34a3eb4/lib/rubocop/ast/processed_source.rb#L260-L325

whitequark/parserの Parser::RubyXX クラスと Prism::Translation::ParserXX クラスとで互換性があることがこの実装からも確認できる。

Prismにも Prism::Translation::ParserCurrent があるけど使えるのは v1.5.0 から

以下で追加されている。

Add `Prism::Translation::ParserCurrent` · ruby/prism@77177f9 · GitHub

残念ながらrbs_railsのprismはv1.2.0でpinされてしまっている(rbs-inlineの依存がそうなっているため)

実装したバージョン分岐ロジック

rbs_rails は Ruby 3.2 未満はサポートしていないので気にしないくていいよ、とレビューコメントもらったので以下のようにした。

      private def parser_class #: untyped
        case RUBY_VERSION
        when /^3\.2\./
          # backward campatibility
          require 'parser/current'
          Parser::CurrentRuby
        when /^3\.3\./
          Prism::Translation::Parser33 # steep:ignore
        when /^3\.4\./
          Prism::Translation::Parser34 # steep:ignore
        else
          # For Prism v1.5.0+, Prism::Translation::ParserCurrent should be used instead.
          Prism::Translation::Parser34 # steep:ignore
        end
      end

Prismのバージョン上げたら Prism::Translation::ParserCurrent を利用するようにしたい。

まとめ

rubocop-astのソースコードリーディングしてparser周りの事情を少し把握できていたのでPR作成までスムーズにいけた。色々興味を持ってソースコードリーディングしておくものですね。