⏲️

Zeitwerk と Ruby のコード読み込みを理解する

2025/03/02に公開

Railsのコードを書いていると、モデルやコントローラのクラスを特にrequireせずに使えます。この挙動を実現しているのがZeitwerkです。この記事では以下のような疑問を解決するつもりです。

  • Zeitwerkって何?何を解決している?
  • Railsはどうやってrequireなしでクラスを読み込んでいる?
  • Zeitwerkの中身はどうなっている?

自分もこれらについて理解を深めたかったので、調べてまとめました。少し長いですが、最後まで読むとZeitwerkの動作原理の一端が理解できると思います!

Zeitwerkとは

Zeitwerkは、Railsで採用されているRubyのコードローダーです。Zeitwerkの主な特徴はこんな感じです。

  1. ファイル名とクラス/モジュール名の規約に基づいて自動読み込み
  2. 開発中のコード変更を検知して再読み込み
  3. 明示的な依存関係宣言が不要

例えば、app/models/user.rbUserクラスがあると、Zeitwerkはそのファイルパスからクラス名を推測し、そのクラスが初めて参照されたときに自動的にファイルを読み込みます。これで、わざわざrequire文を書かなくて済むんですね。

Rubyのコード読み込みの基本

Zeitwerkの仕組みを理解する前に、Rubyの基本的なコード読み込みの仕組みをおさらいしておきます。

requirerequire_relative

Rubyで別ファイルのコードを読み込むには、通常Kernel#requireKernel#require_relativeを使います:

# lib/user.rb
class User
  def initialize(name)
    @name = name
  end

  def greet
    "Hello, #{@name}!"
  end
end

# main.rb
require './lib/user' # カレントディレクトリからの相対パス
# または
# require_relative 'lib/user' # 実行ファイルからの相対パス

user = User.new("Ruby")
puts user.greet # => "Hello, Ruby!"

require$LOAD_PATHと呼ばれる配列に含まれるディレクトリからファイルを探します。見つからなければ指定したパスで探します。一方、require_relativeは現在実行しているファイルからの相対パスでファイルを探します。

Kernel#loadというメソッドもありますが、こちらは毎回ファイルを再読み込みするので、通常は開発中のデバッグなどに使います。

load './lib/user.rb' # 拡張子も必要

Railsにおけるコード読み込み

ここで疑問が湧きます。Railsアプリでは、こんなコードがあります。

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    @users = User.all # Userモデルを明示的にrequireしていない!
  end
end

Userモデルをrequireしていないのに、なぜエラーにならないのでしょう?これがまさに自動読み込み(autoload)の仕組みによるものです。Railsでは、この自動読み込みをZeitwerkが担当しています。

RubyのKernel#autoloadについて

Zeitwerkの仕組みを理解するために、まずRubyの標準機能であるKernel#autoloadについて見ていきましょう。

autoloadは、指定した定数(クラスやモジュール)が初めて参照されたときに、指定したファイルを自動的に読み込む仕組みを提供します:

# autoloadの基本的な使い方
autoload :MyClass, './lib/my_class.rb'

# この時点ではMyClassは読み込まれていない

# MyClassが初めて参照されたとき、./lib/my_class.rbが読み込まれる
obj = MyClass.new

autoloadの良いところは、実際に定数が使用されるまでファイルの読み込みを遅延できること。これで起動時間の短縮やメモリ使用量の最適化ができます。

名前空間付きの定数にも使えます:

module MyNamespace
  autoload :MyClass, './lib/my_namespace/my_class.rb'
end

# MyNamespace::MyClassが参照されたときにファイルが読み込まれる
obj = MyNamespace::MyClass.new

Module#autoloadを使うと、特定のモジュール/クラスのスコープ内で自動読み込みを設定できます:

module MyNamespace
  # MyNamespace::MyClassが参照されたときに読み込まれる
  autoload :MyClass, './lib/my_namespace/my_class.rb'
end

Zeitwerkはどう動いているのか

それでは、Zeitwerkがどのようにautoloadを活用して、Railsの便利な自動読み込みを実現しているのか見ていきましょう。

1. ディレクトリ構造の解析

Zeitwerkはまず、指定されたディレクトリ(Railsならapplibなど)を解析して、ファイルとディレクトリの構造をマッピングします。

例えば、こんなディレクトリ構造があるとしましょう:

app/
  models/
    user.rb
    admin/
      permission.rb

Zeitwerkはこの構造から次のようなマッピングを作ります:

  • Userapp/models/user.rb
  • Adminapp/models/admin (モジュール)
  • Admin::Permissionapp/models/admin/permission.rb

これは、Rails が起動時に Zeitwerk に autoload_paths として app/models などを渡しているためです。
以下のように、ActiveSupport::Dependencies.autoload_paths を Zeitwerk に渡しています。

https://github.com/rails/rails/blob/29c7580f9586db473045bd339bced228ce3b2fa9/railties/lib/rails/application/finisher.rb#L30

2. autoloadの設定

次に、Zeitwerkはこのマッピングを基にautoloadの設定をします。簡略化すると、こんな感じです:

# トップレベルの定数に対するautoload
autoload :User, 'app/models/user.rb'
autoload :Admin, 'app/models/admin.rb' # このファイルは実際には存在しない

# Adminモジュールが読み込まれた後、その中での自動読み込み設定
module Admin
  autoload :Permission, 'app/models/admin/permission.rb'
end

Zeitwerk内の以下のコードの箇所で autoload が実行されています。

https://github.com/fxn/zeitwerk/blob/2120324b37f8832e4476f20b5a4247197f6a64c3/lib/zeitwerk/cref.rb#L47

でも、Adminのようなディレクトリに対応するモジュールのファイルは実際には存在しないことが多いですよね。そこでZeitwerkは、これを自動的に生成します。

3. ディレクトリをモジュールとして読み込む

Adminのようなディレクトリに対応するモジュールファイルが存在しない場合、Zeitwerkはそのモジュールを自動的に定義します:

# Zeitwerkが自動的に生成するコード
module Admin
end

そして、このAdminモジュールに対して、その下の定数(この場合はPermission)のautoload設定を行います。
Zeitwerk では Cref#setModule#const_set を実行して実現しています。

https://github.com/fxn/zeitwerk/blob/2120324b37f8832e4476f20b5a4247197f6a64c3/lib/zeitwerk/cref.rb#L57

4. 定数の解決と読み込み

実際にコード中でAdmin::Permissionが参照されると、まずRubyはAdminモジュールを探します。Zeitwerkによって設定されたautoloadのおかげで、Adminモジュールが自動的に定義されます。

次に、Admin::Permissionが参照されると、Adminモジュール内のPermission定数のautoloadが発動し、app/models/admin/permission.rbが読み込まれます。

5. カスタムインフレクション

Zeitwerkは基本的に、ファイル名をキャメルケースに変換して定数名を推測します。例えば、user.rbUserに、api_client.rbApiClientになります。

でも、特殊な命名規則が必要な場合もあります。そういうときはカスタムインフレクションを設定できます:

# config/initializers/zeitwerk.rb
Rails.autoloaders.main.inflector.inflect(
  "hoge_error" => "HError",
  "api" => "API"
)

これで、hoge_error.rbHErrorとして、api.rbAPIとして解決されます。

6. リロード機能

Zeitwerkの便利な機能のひとつが、コードの再読み込み(リロード)です。開発環境ではコードを変更したあと、自動的に変更が反映されるのでサーバーの再起動が不要になります。

Zeitwerkはどうやってリロードしているのかというと、読み込んだファイルの記録を保持し、それらを一度アンロードしてから再度読み込むことで実現しています:

Rails は rack middleware として、リクエストされたときにファイルに変更があればこの機構を実行するコードを定義しており、リクエストを受けたときにリロードするようになっています。

# 簡略化したリロードの例
loader = Zeitwerk::Loader.new
loader.setup
loader.reload # すべての定数をアンロードし、再度autoload設定を行う

まとめ

Zeitwerkの仕組みを理解できましたか?Zeitwerkは、RubyのKernel#autoload機能をうまく活用して、ファイル構造からクラスやモジュールを自動的に読み込む仕組みを提供しています。おかげで、Railsアプリでは明示的なrequire文を書かなくても、必要な定数が自動的に読み込まれるんですね。

Zeitwerkのおかげでこんな恩恵が受けられます:

  1. 明示的な依存関係の記述が不要 - ファイル構造から自動的に依存関係を解決
  2. 遅延読み込み - 実際に使用されるまでファイルを読み込まない
  3. 再読み込み機能 - 開発環境での迅速なフィードバックループ

Zeitwerkの仕組みを理解することで、Railsアプリのディレクトリ構造と命名規則の重要性も見えてきます。適切な命名規則とディレクトリ構造に従うことで、Zeitwerkは魔法のように働き、我々の開発体験を向上させてくれます。

RailsのAutoloadの「魔法」がどう動いているのか、少しでも理解の助けになれば嬉しいです。Rails以外のRubyプロジェクトでもZeitwerkは使えるので、大規模なRubyアプリケーションを作るときの選択肢のひとつとして覚えておくといいかもしれません。

Discussion