LoginSignup
29
22

More than 3 years have passed since last update.

Rubyにおけるクラス/モジュール定義関連の仕組みと、Ruby on Railsにおけるautoloadの仕組み

Posted at

Ruby における話

モジュールとクラスの違い

モジュールとクラスの違いとして次の項目が挙げられます。

  • どちらもクラスインスタンス(またはモジュールインスタンス)を作れる
  • どちらもメソッドを定義できる
  • モジュールはオブジェクトを作れない
  • クラスはオブジェクトを作れる
irb(main):001:0> module Parent
irb(main):002:1>   class Child
irb(main):003:2>   end
irb(main):004:1> end
=> nil

# どちらもクラスインスタンスを作れる
irb(main):005:0> Parent.class
=> Module
irb(main):006:0> Parent::Child.class
=> Class

# モジュールはオブジェクトを作れない
irb(main):008:0> Parent.new
Traceback (most recent call last):
        2: from /home/vagrant/.rbenv/versions/2.5.5/bin/irb:11:in `<main>'
        1: from (irb):8
NoMethodError (undefined method `new' for Parent:Module)
# クラスはオブジェクトを作れる
irb(main):009:0> Parent::Child.new
=> #<Parent::Child:0x00005587aeab9c90>

モジュールとクラスが定義されると Object に定数が追加される

モジュールやクラスが定義済であるかどうかを判別する方法として defined? メソッドがあります。

未定義の場合は defined?nil を返し、定義されていると "constant" を返します。

# モジュールが未定義の場合、defined? は nil を返し、定義済の場合は "constant" を返す
$ irb
irb(main):001:0> defined?(Hoge)
=> nil
irb(main):002:0> module Hoge; end
=> nil
irb(main):003:0> defined?(Hoge)
=> "constant"

# クラスの場合も同様
$ irb
irb(main):001:0> class Hoge; end
=> nil
irb(main):002:0> defined?(Hoge)
=> "constant"

ここで、定義済の場合に "constant" が返ることと関連がありますが、モジュールとクラスが定義された時に ruby 内部では Object クラスインスタンスに定数(constant) が追加され、定義されたモジュールまたはクラスのクラスインスタンスが値として格納されます。

# モジュールが定義されると Object クラスインスタンスに定数が追加され、クラスインスタンスが値として格納される
$ irb
irb(main):001:0> Object.const_get("Hoge")
Traceback (most recent call last):
        2: from /home/vagrant/.rbenv/versions/2.5.5/bin/irb:11:in `<main>'
        1: from (irb):1
NameError (uninitialized constant Hoge)
irb(main):002:0> module Hoge; end
=> nil
irb(main):003:0> Object.const_get("Hoge")
=> Hoge
irb(main):004:0> Object.const_get("Hoge").class
=> Module

# クラスの場合も同様
$ irb
irb(main):001:0> class Hoge; end
=> nil
irb(main):002:0> Object.const_get("Hoge")
=> Hoge
irb(main):003:0> Object.const_get("Hoge").class
=> Class

クラスやモジュールがネストされた場合は :: が繋がった名前になります。
また Object クラスインスタンスの他に、子となるクラスやモジュールが定義されると、親となるクラスインスタンスにも定数として定義されます。

irb(main):001:0> module Parent
irb(main):002:1>   class Child
irb(main):003:2>   end
irb(main):004:1> end
=> nil
irb(main):005:0> defined?(Parent::Child)
=> "constant"
irb(main):006:0> Object.const_get('Parent::Child')
=> Parent::Child
irb(main):007:0> Parent.const_get('Child')
=> Parent::Child

このようにモジュールやクラスが定義されると Object クラスインスタンスに定数が追加され、defined? メソッドや Object.const_get("Hoge") を実行することで定義済であるかどうかを確認することが出来ます。

  • クラスやモジュールが未定義である場合
    • defined?nil を返す
    • Object.const_get は NameError となる
  • クラスやモジュールが定義されている場合
    • defined?"constant" を返す
    • Object.const_get がクラスインスタンスを返す

require と load の違い

require は外部ファイルに記載された ruby コードを読み込むことが出来るメソッドです。(module メソッドは Kernel モジュールで定義されています(参考))

クラスやモジュールが定義されている場合は、require を実行することで定義されたものが使えるようになります。
require するファイルに p メソッド等の、文字列を出力するコードが書かれている場合は実行されます。

print.rb
class Print
end

p "print.rb"
$ irb
irb(main):001:0> Print
Traceback (most recent call last):
        2: from /home/vagrant/.rbenv/versions/2.5.5/bin/irb:11:in `<main>'
        1: from (irb):1
NameError (uninitialized constant Print)
irb(main):002:0> require './print.rb'
"print.rb"
=> true
irb(main):003:0> Print
=> Print

require により同じファイルが 2 度読み込まれることはありません。
これはグローバル変数 $LOAD_PATH(又は $"$-I) に読み込んだファイルが保存され、読み込み済みかどうかを判定しているためです。
また、require に指定されたファイルはグローバル変数 $: の配列に書かれた順に探索され、最初に見つかったファイルが読み込まれます。

# require で読み込まれたファイルのパスはグローバル変数 $" に保存される
irb(main):007:0> pp $".select {|p| p =~ %r{/print.rb\Z}}
[]
=> []
irb(main):008:0> require './print.rb'
"print.rb"
=> true
irb(main):009:0> pp $".select {|p| p =~ %r{/print.rb\Z}}
["/<PATH_TO_CURRENT_DIRECTORY>/print.rb"]
=> ["/<PATH_TO_CURRENT_DIRECTORY>/print.rb"]

# require に指定したファイル名はグローバル変数 $: で指定されたリストから順に探索される
irb(main):002:0> pp $:
["/home/vagrant/.rbenv/rbenv.d/exec/gem-rehash",
 "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/did_you_mean-1.2.0/lib",
 "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/site_ruby/2.5.0",
 "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/site_ruby/2.5.0/x86_64-linux",
 "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/site_ruby",
 "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/vendor_ruby/2.5.0",
 "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/vendor_ruby/2.5.0/x86_64-linux",
 "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/vendor_ruby",
 "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/2.5.0",
 "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/2.5.0/x86_64-linux"]
=> ["/home/vagrant/.rbenv/rbenv.d/exec/gem-rehash", ...]
# グローバル変数 $: にカレントディレクトリは無いため、ファイル名だけでは読み込めない
irb(main):010:0> require 'print.rb'
Traceback (most recent call last):
        4: from /home/vagrant/.rbenv/versions/2.5.5/bin/irb:11:in `<main>'
        3: from (irb):10
        2: from /home/vagrant/.rbenv/versions/2.5.5/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'
        1: from /home/vagrant/.rbenv/versions/2.5.5/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'
LoadError (cannot load such file -- print.rb)

一方で、 load は何度実行してもファイルが読み込まれます。
また load に指定したファイル名は拡張子 .rb.so が補完されず、読み込んだファイルのパスは $" に追加されません。

require メソッドはライブラリの読み込みに使われ、load は設定ファイルの読み込みに使われることが想定されています。

my_class.rb
class MyClass
  def self.hello
    p 'hello'
  end
end
irb(main):001:0> load './my_class.rb'
=> true
irb(main):003:0> load './my_class.rb'
=> true
irb(main):004:0> MyClass.hello
"hello"
=> "hello"

# ここで my_class.rb が更新された場合は load により変更が反映される
#   my_class.rb の更新内容は self.hello メソッドが返す文字列が "hello" -> "hello world" に変わったことです
irb(main):006:0> load './my_class.rb'
=> true
irb(main):007:0> MyClass.hello
"hello world"
=> "hello world"

# `load` により読み込んだファイルのパスは `$"` に追加されない
$ irb
irb(main):001:0> load './my_class.rb'
=> true
irb(main):002:0> $".select {|p| p =~ /my_class.rb\Z/}
=> []
irb(main):003:0> require './my_class.rb'
=> true
irb(main):004:0> $".select {|p| p =~ /my_class.rb\Z/}
=> ["/<PATH_TO_CURRENT_DIRECTORY>/my_class.rb"]

このように requireload メソッドを使うことで外部ファイルを読み込むことが出来ますが次のような違いがあります。

  • requireload もグローバル変数 `$:" に指定されたパスの配列から順に探索する
  • require はライブラリの読み込み、load は設定ファイルの読み込みに使うよう想定されている
    • require で指定したファイルは拡張子 .rb.so が補完される
    • laod で指定したファイルは拡張子は補完されない
  • require は同じファイルを 1 度だけ読み込む
    • 既に読み込んだファイルはグローバル変数 $" に保存される
  • load は無条件に読み込む
    • 読み込んだファイルはグローバル変数 $" に保存されない

Ruby on Rails における話

bundle したファイルが require される仕組み

参考として、Rails が Bundler でインストールした gem を require せずに使えている仕組みを説明します。

app/config/application.rb にその処理が書かれています。

app/config/application.rb
Bundler.require(*Rails.groups)

Rails.groups は RAILS_ENV により以下のように設定されます。

# RAILS_ENV=development の場合
irb(main):009:0> Rails.groups
=> [:default, "development"]

# RAILS_ENV=production の場合
irb(main):002:0> Rails.groups
=> [:default, "production"]

尚、Gemfile に require: false が指定された場合は Bundler.require により require する対象から外れます。(参照)

autoload の仕組み

Rails6 から自動読み込みの新しいモード Zeitwerk が追加されました。
ここでは Rails6 より前の Classic モードにおける自動読み込み(参照
)について説明します。

autoload とは Ruby on Rails におけるソースコードに変更が加えられた時に自動読み込みする機能です。

例えば User モデルが app/models/user.rb で定義されている場合、require を指定せずに Rails アプリケーション内で User を呼び出すことが出来ます。

$ bin/rails c
# 未定義である状態で `User` モデルを使おうとすると、定義が自動で読み込まれて定義済となる
irb(main):001:0> Object.constants.select{|p| p == :User}
=> []
irb(main):002:0> User.column_names
=> ["id", "name", "created_at", "updated_at"]
irb(main):003:0> Object.constants.select{|p| p == :User}
=> [:User]

# グローバル変数 $" にパスは追加されない
irb(main):004:0> $".select{|p| p =~ /user.rb\Z/}
=> []

この自動読み込みは development 環境ではデフォルト有効になっており、production 環境では無効化されています。(Rails5 でのデフォルト設定。Rails4 でのデフォルト設定は異なるようです。)

自動読み込みする対象となるファイルは config.autoload_paths に格納されています。
これは require で使う $LOAD_PATH とは異なる値が格納されています。

デフォルトで config.autoload_paths に設定されているのは次のディレクトリです。(Rails5 でのデフォルト設定)

  • app 配下の第1ディレクトリ
  • app/*/concerns ディレクトリ
  • test/mailer/previews ディレクトリ

Rails における自動読み込み処理の順序は次のとおりです。

尚、Rails における autoload ではクラスインスタンスやモジュールインスタンスは定数として取り扱います。
そのため以降の自動読み込みの説明においてもクラスインスタンスやモジュールインスタンスは定数として説明します。
(定数に対して autoload が働くため、クラスやモジュールではない一般的な定数も同じ仕組みで autoload されます)

  1. Ruby インタプリタにおける定数が未定義の場合に発生する const_missing をフックして Rails における自動読み込みをトリガーする
    • ruby インタプリタは module, class キーワードの後ろに置かれる定数が未定義であれば定義を行う
    • ruby インタプリタにより定義済と判定された定数は Rails の自動読み込みは行われない
  2. 呼び出し元のモジュール名を基準として、定数の名前を特定する
    • 無名(self.name == nil)のモジュールで定義された定数の場合は、呼び出し元のモジュールは "Object" となる
    • 定数が登場した箇所のネストに基づく名前となる
  3. 定数の名前からファイル名を特定する (定数とファイル名は 定数名.underscore == ファイル名 の関係)
  4. ファイルが見つかった場合、load 又は require によりファイルが読み込まれる
    • ENV["NO_RELOAD"] が指定されている場合は require が使われ、それ以外は laod が使われる
  5. ファイルが見つからないが、自動読み込み対象のモジュールである場合は該当する名前のモジュールを定義する
    • 自動読み込み対象のモジュールは autoload_paths 配下にあるディレクトリを指す
    • 例えば、app/my_dir/my_class.rb がある場合 MyDir という定数は自動読み込み対象のモジュールとして、Rails の自動読み込みによりモジュールとして定義される
  6. ファイルが見つからず、自動読み込み対象のモジュールでもない場合は、親となるモジュールを基準として読み込み処理を繰り返す(上の2.から実行する)

参考: autoload は AutoSupport::Dependencies にて const_missing メソッドをオーバーライドすることで実装されています。

例えばアプリケーションに app/models/user.rb が存在する場合に、コントローラ内で User を指定すると自動読み込みがどの順番で行われるか見ていきます。

app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts = User.current_user.posts
  end
end

上の例で定数 User が自動読み込みされる場合を考えてみます。
すると、次の順序で自動読み込みが行われます。

  1. Ruby インタプリタは Object クラスインスタンスに User が存在しないとして const_missing を呼び出す
  2. Rails が const_missing をフックして自動読み込みをトリガーする
  3. 呼び出し元のモジュール名を基準として、定数の名前を特定する
    • 定数が登場した箇所のネストに基づく名前となる
      • PostsController::User
  4. 定数の名前からファイル名を特定する (定数とファイル名は 定数名.underscore == ファイル名 の関係)
    • posts_controller/user.rb を autoload_paths から探す
  5. ファイルが見つからず、自動読み込み対象のモジュールでもない場合は、親となるモジュールを基準として読み込み処理を繰り返す
    • ::User
  6. 定数の名前からファイル名を特定する (定数とファイル名は 定数名.underscore == ファイル名 の関係)
    • user.rb を autoload_paths から探す
  7. ファイルが見つかった場合、load 又は require によりファイルが読み込まれる
    • app/models/user.rb が見つかり load される

以上が Rails における自動読み込みの仕組みです。

  • Rails における自動読み込み(autoload)は ActiveSupport::Dependencies にて実装されている
  • autoload の対象となるファイルは config.autoload_paths から検索できるものである
  • ファイルは load により読み込まれる (require は環境変数を指定した時に使われる)
  • ファイルではなくディレクトリであった場合は、自動読み込みモジュールとして定義される
  • 定数が登場した箇所のネストに基づく名前を使った検索で見つからなかった場合は、親の名前空間を基準として検索が続けられる

更新されたファイルを再度読み込む仕組み

先に説明した autoload のアルゴリズムはあくまで未定義の定数を再読み込みする仕組みです。

Rails アプリケーションを開発していて RAILS_ENV=development でアプリケーションを動作させた時に、ファイルが更新されると自動で変更が反映されていることに気が付くと思います。

これは Rails アプリケーションのファイル更新をウォッチする config.file_watcher により、ファイルが更新されたことをトリガーとして、autoload されたファイル一覧が空に初期化され、定数の削除が行われることで実現します。

つまり、定義済の状態を強制的に未定義の状態にすることで、次に定数が読み込まれるタイミングで autoload が実行されることが期待され、autoload により最新のファイルが読み込まれることで実現されています。

reload! の仕組み

reload! は定数の再読み込みを行うメソッドです。

development モードで動作する Rails アプリケーションは、ファイルが変更されるとクラスやモジュールを自動的に再読み込みします。(config.cache_classes が false なら自動読み込みが有効になり、config.cache_classes が true なら自動読み込みが無効になる)

しかし Rails console では逆に一貫した状態であることが望まれるため、config.cache_classes の値によらず再読み込みが行われません。

そこで reload! メソッドを実行することで強制的に再読み込みすることが出来ます。

これは Rails の内部で autoload されたファイル一覧が空に初期化され、また各クラスインスタンスに登録された定数が削除されるため強制的に再読み込みが出来ます。

注意点として既に変数にクラスインスタンスが代入されている場合、再読み込みが行われても変数内の状態は更新されません。

app/models/user.rb
class User < ApplicationRecord
  def self.num
    1
  end
end
irb(main):001:0> user = User
=> User (call 'User.connection' to establish a connection)
irb(main):002:0> user.num
=> 1

# ここで、User.num が 2 を返すように修正する
irb(main):003:0> reload!
Reloading...
=> true

# 既に変数に保存されたクラスインスタンスは変更されない
irb(main):004:0> user.num
=> 1
# 新たに参照されたクラスインスタンスは変更される
irb(main):005:0> User.num
=> 2
29
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
22