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
メソッド等の、文字列を出力するコードが書かれている場合は実行されます。
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
は設定ファイルの読み込みに使われることが想定されています。
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"]
このように require
と load
メソッドを使うことで外部ファイルを読み込むことが出来ますが次のような違いがあります。
-
require
もload
もグローバル変数 `$:" に指定されたパスの配列から順に探索する -
require
はライブラリの読み込み、load
は設定ファイルの読み込みに使うよう想定されている-
require
で指定したファイルは拡張子.rb
や.so
が補完される -
laod
で指定したファイルは拡張子は補完されない
-
-
require
は同じファイルを 1 度だけ読み込む- 既に読み込んだファイルはグローバル変数
$"
に保存される
- 既に読み込んだファイルはグローバル変数
-
load
は無条件に読み込む- 読み込んだファイルはグローバル変数
$"
に保存されない
- 読み込んだファイルはグローバル変数
Ruby on Rails における話
bundle したファイルが require される仕組み
参考として、Rails が Bundler でインストールした gem を require せずに使えている仕組みを説明します。
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 されます)
- Ruby インタプリタにおける定数が未定義の場合に発生する const_missing をフックして Rails における自動読み込みをトリガーする
- ruby インタプリタは module, class キーワードの後ろに置かれる定数が未定義であれば定義を行う
- ruby インタプリタにより定義済と判定された定数は Rails の自動読み込みは行われない
- 呼び出し元のモジュール名を基準として、定数の名前を特定する
- 無名(
self.name == nil
)のモジュールで定義された定数の場合は、呼び出し元のモジュールは"Object"
となる - 定数が登場した箇所のネストに基づく名前となる
- 無名(
- 定数の名前からファイル名を特定する (定数とファイル名は
定数名.underscore == ファイル名
の関係) - ファイルが見つかった場合、
load
又はrequire
によりファイルが読み込まれる-
ENV["NO_RELOAD"]
が指定されている場合はrequire
が使われ、それ以外はlaod
が使われる
-
- ファイルが見つからないが、自動読み込み対象のモジュールである場合は該当する名前のモジュールを定義する
- 自動読み込み対象のモジュールは autoload_paths 配下にあるディレクトリを指す
- 例えば、
app/my_dir/my_class.rb
がある場合MyDir
という定数は自動読み込み対象のモジュールとして、Rails の自動読み込みによりモジュールとして定義される
- ファイルが見つからず、自動読み込み対象のモジュールでもない場合は、親となるモジュールを基準として読み込み処理を繰り返す(上の2.から実行する)
参考: autoload は AutoSupport::Dependencies
にて const_missing
メソッドをオーバーライドすることで実装されています。
- ActiveSupport::Dependencies::ModuleConstMissing.const_missing
- ActiveSupport::Dependencies.load_missing_constant
例えばアプリケーションに app/models/user.rb
が存在する場合に、コントローラ内で User を指定すると自動読み込みがどの順番で行われるか見ていきます。
class PostsController < ApplicationController
def index
@posts = User.current_user.posts
end
end
上の例で定数 User
が自動読み込みされる場合を考えてみます。
すると、次の順序で自動読み込みが行われます。
- Ruby インタプリタは Object クラスインスタンスに User が存在しないとして
const_missing
を呼び出す - Rails が
const_missing
をフックして自動読み込みをトリガーする - 呼び出し元のモジュール名を基準として、定数の名前を特定する
- 定数が登場した箇所のネストに基づく名前となる
- →
PostsController::User
- →
- 定数が登場した箇所のネストに基づく名前となる
- 定数の名前からファイル名を特定する (定数とファイル名は
定数名.underscore == ファイル名
の関係)- →
posts_controller/user.rb
を autoload_paths から探す
- →
- ファイルが見つからず、自動読み込み対象のモジュールでもない場合は、親となるモジュールを基準として読み込み処理を繰り返す
- →
::User
- →
- 定数の名前からファイル名を特定する (定数とファイル名は
定数名.underscore == ファイル名
の関係)- →
user.rb
を autoload_paths から探す
- →
- ファイルが見つかった場合、
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 されたファイル一覧が空に初期化され、また各クラスインスタンスに登録された定数が削除されるため強制的に再読み込みが出来ます。
注意点として既に変数にクラスインスタンスが代入されている場合、再読み込みが行われても変数内の状態は更新されません。
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