Zeitwerk と Ruby のコード読み込みを理解する
Railsのコードを書いていると、モデルやコントローラのクラスを特にrequire
せずに使えます。この挙動を実現しているのがZeitwerkです。この記事では以下のような疑問を解決するつもりです。
- Zeitwerkって何?何を解決している?
- Railsはどうやってrequireなしでクラスを読み込んでいる?
- Zeitwerkの中身はどうなっている?
自分もこれらについて理解を深めたかったので、調べてまとめました。少し長いですが、最後まで読むとZeitwerkの動作原理の一端が理解できると思います!
Zeitwerkとは
Zeitwerkは、Railsで採用されているRubyのコードローダーです。Zeitwerkの主な特徴はこんな感じです。
- ファイル名とクラス/モジュール名の規約に基づいて自動読み込み
- 開発中のコード変更を検知して再読み込み
- 明示的な依存関係宣言が不要
例えば、app/models/user.rb
にUser
クラスがあると、Zeitwerkはそのファイルパスからクラス名を推測し、そのクラスが初めて参照されたときに自動的にファイルを読み込みます。これで、わざわざrequire
文を書かなくて済むんですね。
Rubyのコード読み込みの基本
Zeitwerkの仕組みを理解する前に、Rubyの基本的なコード読み込みの仕組みをおさらいしておきます。
require
とrequire_relative
Rubyで別ファイルのコードを読み込むには、通常Kernel#require
かKernel#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が担当しています。
Kernel#autoload
について
Rubyの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ならapp
、lib
など)を解析して、ファイルとディレクトリの構造をマッピングします。
例えば、こんなディレクトリ構造があるとしましょう:
app/
models/
user.rb
admin/
permission.rb
Zeitwerkはこの構造から次のようなマッピングを作ります:
-
User
→app/models/user.rb
-
Admin
→app/models/admin
(モジュール) -
Admin::Permission
→app/models/admin/permission.rb
これは、Rails が起動時に Zeitwerk に autoload_paths として app/models
などを渡しているためです。
以下のように、ActiveSupport::Dependencies.autoload_paths
を Zeitwerk に渡しています。
autoload
の設定
2. 次に、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 が実行されています。
でも、Admin
のようなディレクトリに対応するモジュールのファイルは実際には存在しないことが多いですよね。そこでZeitwerkは、これを自動的に生成します。
3. ディレクトリをモジュールとして読み込む
Admin
のようなディレクトリに対応するモジュールファイルが存在しない場合、Zeitwerkはそのモジュールを自動的に定義します:
# Zeitwerkが自動的に生成するコード
module Admin
end
そして、このAdmin
モジュールに対して、その下の定数(この場合はPermission
)のautoload
設定を行います。
Zeitwerk では Cref#set
で Module#const_set
を実行して実現しています。
4. 定数の解決と読み込み
実際にコード中でAdmin::Permission
が参照されると、まずRubyはAdmin
モジュールを探します。Zeitwerkによって設定されたautoload
のおかげで、Admin
モジュールが自動的に定義されます。
次に、Admin::Permission
が参照されると、Admin
モジュール内のPermission
定数のautoload
が発動し、app/models/admin/permission.rb
が読み込まれます。
5. カスタムインフレクション
Zeitwerkは基本的に、ファイル名をキャメルケースに変換して定数名を推測します。例えば、user.rb
はUser
に、api_client.rb
はApiClient
になります。
でも、特殊な命名規則が必要な場合もあります。そういうときはカスタムインフレクションを設定できます:
# config/initializers/zeitwerk.rb
Rails.autoloaders.main.inflector.inflect(
"hoge_error" => "HError",
"api" => "API"
)
これで、hoge_error.rb
はHError
として、api.rb
はAPI
として解決されます。
6. リロード機能
Zeitwerkの便利な機能のひとつが、コードの再読み込み(リロード)です。開発環境ではコードを変更したあと、自動的に変更が反映されるのでサーバーの再起動が不要になります。
Zeitwerkはどうやってリロードしているのかというと、読み込んだファイルの記録を保持し、それらを一度アンロードしてから再度読み込むことで実現しています:
Rails は rack middleware として、リクエストされたときにファイルに変更があればこの機構を実行するコードを定義しており、リクエストを受けたときにリロードするようになっています。
# 簡略化したリロードの例
loader = Zeitwerk::Loader.new
loader.setup
loader.reload # すべての定数をアンロードし、再度autoload設定を行う
まとめ
Zeitwerkの仕組みを理解できましたか?Zeitwerkは、RubyのKernel#autoload
機能をうまく活用して、ファイル構造からクラスやモジュールを自動的に読み込む仕組みを提供しています。おかげで、Railsアプリでは明示的なrequire
文を書かなくても、必要な定数が自動的に読み込まれるんですね。
Zeitwerkのおかげでこんな恩恵が受けられます:
- 明示的な依存関係の記述が不要 - ファイル構造から自動的に依存関係を解決
- 遅延読み込み - 実際に使用されるまでファイルを読み込まない
- 再読み込み機能 - 開発環境での迅速なフィードバックループ
Zeitwerkの仕組みを理解することで、Railsアプリのディレクトリ構造と命名規則の重要性も見えてきます。適切な命名規則とディレクトリ構造に従うことで、Zeitwerkは魔法のように働き、我々の開発体験を向上させてくれます。
RailsのAutoloadの「魔法」がどう動いているのか、少しでも理解の助けになれば嬉しいです。Rails以外のRubyプロジェクトでもZeitwerkは使えるので、大規模なRubyアプリケーションを作るときの選択肢のひとつとして覚えておくといいかもしれません。
Discussion