LoginSignup
7
4

More than 1 year has passed since last update.

Kernel#loadは何をしているのか

Last updated at Posted at 2022-02-06

loadの使い所ってどこなの?

いろいろあって気づいたら年が明けてしまいましたが、Ruby3.1出ています。
関連記事でもいろいろと紹介されている通り、irbやデバッガ周り、ハッシュの省略記法などが入ってますね!

遅ればせながらプロと読み解く Ruby3.1 NEWSを見ている時に、ふとこの仕様が目に止まりました。

Kernel#loadの第二引数で任意のモジュールを指定できるようになった

Kernel#load now accepts a module as the second argument, and will load the file using the given module as the top-level module. [Feature #6210]

へえ、このオプション、なぜloadだけに入ってrequireには入らないのだろう?
そもそもどういうユースケースのために作られた機能なの?

そんな疑問をかかえながら、動作を確認したり、サンプルコードを書いていたらすっかり深みにハマりました。
使われない子のイメージが強いKernel#loadですが、みなさんは一体何をするものかご存知でしょうか・・・?

というわけで、誰も掘らないようなニッチを関心ごとをひたすら掘り下げる**「何をしているのか」シリーズ**、やっていきたいと思います!
(なお、まったく実務の役には立つものではありませんので悪しからず)

load vs require

以下はすべてRuby3.1で確認しています。

まずは恒例のRTFMということで、るりまでKernel#loadKernel#requireについて見てみると、ざっくりこんなことが書いてあります1
みんな一度は調べたことがあるのではないでしょうか。

load  require
ファイルロード 無条件に何度でも 一度だけ
拡張子 拡張子が必要 補完する(.rb, .so)
ファイル探索 $LOAD_PATHより探索 $LOAD_PATHより探索

loadのわかりやすい特徴は、ファイルを何度でもロードできる、ということにつきます。
本番環境のアプリケーションは一度だけロードされることを保証したいため、一般にはrequire(require_relative)を使う、と言うのが相場になっています。

では、いったいloadってどう言う時に使うものだろう?

Loadの動き方をほりさげる

ここからは、いままで気にしたことがないloadの挙動を考えてみたいと思います。

ドキュメントを読む

るりまにはまだ記述がないようなので、英語版のドキュメントをみていきましょう。
(RDocで生成されているので、ソースコード内の最新コメントが反映されています)

loadの第二引数としてwrapというパラメータが渡せるということになっています。

load(filename, wrap=false) → true

注目は最終行あたりにあるこの記述です。

If the optional wrap parameter is true, the loaded script will be executed under an anonymous module, protecting the calling program’s global namespace.
If the optional wrap parameter is a module, the loaded script will be executed under the given module.
In no circumstance will any local variables in the loaded file be propagated to the loading environment.

かいつまんで訳すと、wrapというパラメータについて

  • 無指定またはfalseの場合はデフォルトの挙動をする
  • trueの場合、ロードされたスクリプトは無名モジュール内で実行される。これによりグローバルなネームスペースを汚染しない
  • モジュールを指定した場合、ロードされたスクリプトはそのモジュール内で実行される。(← これがRuby3.1からの新機能)

となっています。
また、「いかなる場合においても、(ロードされたファイルの)ローカル変数が呼び出した側の環境に波及することはない」ということも明記されています。

コードを動かしてみる

早速、確認してみましょう。
まずロード対象のスクリプトを書いておきます。

# loadable.rb
MY_CONST = 'a constant'
@instance_var = 'an instance var'
local_var = 'local_var'

def some_method
  puts 'here I am!'
end

このファイル loadble.rbをオプション違いでロードして、定数やインスタンス変数、ローカル変数、メソッドがそれぞれどうなるかをirbで見てみましょう。

1. デフォルト(wrap=false)の場合

load 'loadable.rb' # => true

MY_CONST
# => "a constant"

@instance_var
# => "an instance var"

local_var
# => undefined local variable or method `local_var' for main:Object (NameError)

some_method
here I am!

ローカル変数を除き、すべてトップレベルからアクセスできます。
このオプションを指定した場合の挙動は、雑な言い方をすると「ファイルロックをしないrequire」という理解が当てはまりそうですね。

蛇足ですが、これらはすべてObjectに定義されています。

Object.constants.grep /MY_CONST/                          #=> [:MY_CONST]
Object.private_instance_methods(false).grep /some_method/ # => [:some_method]

ただし、インスタンス変数だけはObjectには持っておらず、トップレベルのselfであるmain固有のものになっています。

self.instance_eval { @instance_var }       # => "an instance var"
Object.class_eval { @instance_var }        # => nil
Object.new.instance_eval { @instance_var } # => nil

2. wrap=trueを指定した場合

もう一つ、古くからある別のオプションをみてみましょう。
(フレッシュな環境で試すため、irbは再起動しています)

load 'loadable.rb', true # => true

MY_CONST
# => uninitialized constant MY_CONST (NameError)

@instance_var
# => nil

local_var
# => undefined local variable or method `local_var' for main:Object (NameError)

some_method
# => undefined local variable or method `some_method' for main:Object (NameError)

この辺はモジュール内にロードしたパターンと一緒ですね。
今度は定数やメソッドはどこへいったのでしょうか。

Object::MY_CONST
# => uninitialized constant MY_CONST (NameError)

Object.send :some_method
# => undefined method `some_method' for Object:Class (NoMethodError)

トップレベルの親であるObjectクラスにいるわけでもなさそうですね。
ロードされたファイル内で定義されたものたちの痕跡が全く残らないようです。

3. wrapとしてモジュールを指定した場合

一方、Ruby3.1で追加されたモジュールオプションを指定するとこんな感じです。

module LoadBase; end
# このモジュールにロードする
load 'loadable.rb', LoadBase # => true

MY_CONST
# => uninitialized constant MY_CONST (NameError)

@instance_var
# => nil

local_var
# => undefined local variable or method `local_var' for main:Object (NameError)

some_method
# => undefined local variable or method `some_method' for main:Object (NameError)

同じくトップレベルにはロードした痕跡がなくなりました。

ロードされた定数や変数はLoadBaseの中にいることが確認できます。
(トップレベルの可視性がprivateになっている関係で、メソッドはprivateメソッドとして登録されます)

LoadBase.constants.grep /MY_CONST/                          #=> [:MY_CONST]
LoadBase.private_instance_methods(false).grep /some_method/ # => [:some_method]

つまりグローバルのネームスペースを汚すことなく、ロードした定数やメソッドを指定したモジュールに定義してくれる、という挙動が確認できます。

一方、インスタンス変数は行方不明です。

LoadBase.module_eval { @instance_var } # => nil

実はこれ後で触れますが(めちゃくちゃ頑張れば)取り出すことはできます。

ここで「いなくなった奴らがどこにいるか」ということを掘る前に、ここまでの挙動をまとめておきましょう。

loadの挙動の違い

ここまでわかった定数・メソッド・変数の定義先の違い2を、オプションのタイプごとにまとめてみます3

wrapオプション 定数 メソッド インスタンス変数 ローカル変数
false(デフォルト) Object Objectのprivateインスタンスメソッド main
true
モジュール モジュール モジュール

とある部分は普通には取り出せないもの、つまり設計上は呼び出し側で再利用することが意図されていないものと考えられそうです。

せっかく調べたので、ここから逆算してそれぞれどんな使い方をするための機能なのか、ということを推し量ってみましょう。

  • falseyなオブジェクト(つまりfalse以外ではnil)だった場合、デフォルトオプションと同じ挙動をします。
  • モジュールではないオブジェクトを渡した場合、trueを渡したのと同じ挙動をします。
    つまり、モジュールを渡すつもりで うっかりクラスをオプションとして渡した場合、wrap=trueとして扱われ、渡したクラスにメソッドが生えることはありません。

1. デフォルトオプション(wrap=false)のユースケース

まずデフォルトのloadは、雑に表現すると「再ロード可能なrequire」のような感じで捉えると良さそうです。
定数やメソッドを外のファイルから繰り返し投入することができるので、開発環境の再ロードなどを意図して作られているように思えます。

ただ実際にrequireに代えてloadを使うか・・と言われると少し戸惑う部分があります。
仮に何度もロードしたいという場合でも、実は後述するように$LOADED_FEATURESの操作をすることで再requireすることは可能です。

さらに、requireにあってデフォルトオプションのロードにないものとして、

  • ファイルロック機構を利用して、複数スレッドで同時にロードが走った場合に競合を避けるしくみがある
  • バイナリ形式の拡張ライブラリ(linuxの場合.soファイル)も呼び出せる
  • 拡張子を省略できる

という特性があります。
ここであえてloadを使いたいというユースケースってなんでしょうね・・・うーん。

github上をあれこれ掘ってみたところ、唯一Kernel#loadを使ってるケースをrougeというgemで発見しました。

自身のプログラムをリロードするために利用しているようですね。

# https://github.com/rouge-ruby/rouge/blob/442c8f430277889afc6ce0ccbbdbdaec687abc0f/lib/rouge.rb
module Rouge
  class << self
    def reload!
      Object::send :remove_const, :Rouge
      Kernel::load __FILE__
    end
  end
end

このように単独ライブラリ内で自己再ロードを実装する場合、コーナーケースを捨てることができる状況であれば「loadの方が実装が簡易になる」という割り切りがあるかもしれません。

2. wrap=trueのユースケース

こちらはどうでしょう。ファイルで定義されたクラスやメソッドといった一切の定義が残りません。
つまり「別のファイルに定義されているモジュールやクラスをロードして使う」という、requireのようなユースケースでは利用できないことになります。

逆に一切の定義を残さないのであれば、いったい何ができるのでしょうか・・?

そう、何かを画面に表示したり、グローバルなクラスに初期値を設定したり、副作用を起こすことが可能です。
たとえば設定用のスクリプトを読み込むことで挙動を切り替えたい、というケースがあるなら使えるかもしれませんね。

実際にやってみましょう。
呼び出し側に、何の「設定」を記憶させておくための仕掛けを用意します。

class Something
  class << self
    attr_accessor :option, :version
  end
end

# Something.option = :hoge とやることで、外からオプションを設定することが可能

さて、ロードする設定ファイルとして2パターン用意します。

# setting1.rb

# グローバル名前空間を汚さないことを見るためにあえて定数も設定しておく
VERSION = __FILE__

puts "#{VERSION} is loaded!"
Something.option  = :foo
Something.version = VERSION
# setting2.rb

# グローバル名前空間を汚さないことを見るためにあえて定数も設定しておく
VERSION = __FILE__

puts "#{VERSION} is loaded!"
Something.option  = :bar
Something.version = VERSION

では、まずsetting1.rbを呼び出してみましょう。

> load 'setting1.rb', true
# setting1.rb is loaded!

Something.version # => "setting1.rb"
Something.option  # => :foo

次にもう一つのファイルsetting2.rbをロードしてみましょう。

# loadで設定を切り替えできる
load 'setting2.rb', true
# setting2.rb is loaded!

# ロードしたスクリプトによりSomethingの状態が変化している
Something.version # => "setting2.rb"
Something.option  # => :bar

もちろんロードするたびに何度でも切り替え可能です。

load 'setting1.rb', true
# setting1.rb is loaded!

# ロードしたスクリプトによりSomethingの状態が変化している
Something.version # => "setting1.rb"
Something.option  # => :foo

こんな感じで、切り替えが効きます。
またロードされたファイル内で定義された定数はトップレベルから参照できません。

# 定数定義などの痕跡は残さない
VERSION           # => uninitialized constant VERSION (NameError)

「グローバルネームスペースを汚さず、別ファイルに定義された設定の切り替えを何度も行いたい」みたいな用途だと、使えるのでしょうか。

とはいえ・・・うーん。

通常、設定情報を別ファイルに置く場合、yamlなどで設定データだけを分離しそうです。
プログラム(Something)側で、データを呼び出して挙動を切り替えられるようにしますよね。

データとプログラムを分離しておいた方が、本体と設定ファイルが密結合になるのを避けられてメンテしやすそうです。

強いて言うと・・・うーん・・そうですね。
設定時に動的に何かを計算する必要があり、さらに「その計算が設定ファイルごとに異なっていてスクリプトとしてしか書けない」「データと計算ロジックは全部外のファイルに出しておきたい」というような場合には有効かもしれません。
(そんなケースがあるかわからないけどもはや意地)

何しろトップレベルに痕跡を全く残さないという、requireにはない唯一無二の機能なので、そこに当てはまるユースケースが見つかれば面白そうです。

3. モジュールを指定するパターンのユースケース

モジュール付きは、新機能の紹介で書かれているようにDSLなどのユースケースが想定されそう。
コレは一番想像がしやすいですね。

たとえばすごい雑な例を作ると・・・こういうことでしょうか。

# dsl.rb

def store(data)
  @store ||= []
  @store << data
end

def show_store
  puts "stored: #{@store}"
end

それではコレを呼び出してみます。

module DSL; end

load 'dsl.rb', DSL

必要なクラスでこのモジュールをextendすることで、定義したdslが使えることがわかります。

class Foo
  extend DSL

  store :foo
  show_store

  store :bar
  store :baz
  show_store
end

# stored: [:foo]
# stored: [:foo, :bar, :baz]
# => nil

特に意味のない機能ですが、期待通りに動作している様子はわかりますね。
ただこれと同じことを実現するなら、moduleごと別ファイルに書いておき、まるっとrequireする形で実現可能です。

loadを使う意味があるとしたら、うーん・・・実行中に異なる種類のDSLを切り替えたいという特殊なケースがあるのでしょうか(捻り出した)

雑なDSLを定義したファイルを用意します。

# dsl1.rb

def store(data)
  @store ||= []
  @store << data
  @store.uniq!
end

def show_store
  puts "stored: #{@store}"
end

もう一つ、別パターンのDSLを定義したファイルを用意します。

# dsl2.rb

def store(key)
  @store ||= {}
  @store[key] = @store[key].to_i + 1
end

def show_store
  puts "stored: #{@store}"
end

loadを利用することで、この2種類のDSLを切り替えることができます。

module DSL; end

class Foo
  extend DSL

  load 'dsl1.rb', DSL
  store :foo
  store :bar
  store :bar
  show_store  # stored: [:foo, :bar]

  # 切り替える
  load 'dsl2.rb', DSL
  store :bar
  store :bar
  store :baz
  show_store  # stored: {:bar=>2, :baz=>1}


  # また戻す
  load 'dsl1.rb', DSL
  store :baz
  show_store  # stored: [:foo, :bar, :baz]
end

という少々トリッキーな手法ですが、見事に2種類のDSLを切り替えることができるようになりましたね。

前の2例が若干苦しかったのに対して、こちらも苦・・いや、なにかストラテジーパターン的な面白い使い道があるようにも思います。

生まれたての新機能なので、今後どんな利用法が見出されていくのか楽しみでもありますね。

まとめ

そんなわけで、loadを使ったことがない自分が、完全なる想像に基づくユースケースを考えてみたものをまとめます。ただただ自信がない。
他にも、こんな利用法があるよ!というアドバイスがあればぜひツッコミいただけると助かります(ペコリ

wrapオプション 想像される用途 定数 メソッド インスタンス変数 ローカル変数
false(デフォルト) 簡易的な再ロード Object Objectのprivateインスタンスメソッド main
true スクリプトによる動的な設定切り替え(?)
モジュール 切り替えのできるDSL(?) モジュール モジュール

おめでとうございます。ついにKernel#loadを完全に理解できました!

(番外編)

さてここから先は、わずかにあった実用性もかなぐり捨てて、好奇心のおもむくままにもう少しだけ仕様を掘り下げてみます(読者いるの)

Loadはどこに定義を隠すのか

上の表でとなっているものたちは、ロード後にいったいどこに消えたのでしょうか?

Rubyでは全てがオブジェクトのはずで、「定義したインスタンス変数がどこにも所属していない」という状況は考えづらいです。
また定義したメソッドや定数も、実行時のクラス(cref)に生えるはずなのですが、ロード後には取り出せなくなってしまいます。

実は、これらはなかなか巧妙な手法によって(しかし、ごくごく真っ当なRubyの仕様の中で)実現されています。
ソースコードを見ていきましょう。

ロードの仕組み

CRubyにおけるloadの実体はrb_load_internalという関数にあります。

wrapオプションを設定した際に、一体何が起こっているかをRuby3.1のソースコードから読んでみましょう。
ポイントは大きく3つあります。

1. ラッパーモジュール(ネームスペース)が作られる

ここでは、オプションが存在していて(truthyで)かつモジュールではない場合、無名モジュールを作る処理が書かれているのがわかります。

    if (RTEST(wrap)) {                    /* wrapパラメータがtruthy(false, nil以外)だった場合 */
        if (!RB_TYPE_P(wrap, T_MODULE)) { /* wrapがモジュールでなかった場合 */
            wrap = rb_module_new();       /* 無名モジュールを生成 */
        }
        state = load_wrapping(ec, fname, wrap);
    }

もちろんオプションにモジュールを設定した場合、そのモジュール自体がwrapとして設定されています。

以前のアドベントでこまごまと分析したのですが、Rubyは定数定義を見つけると、実行時のcrefが指すクラスに対して定義をするはずです。

このwrapモジュールがロードされたファイル内での定数定義を引き受けることで、トップレベルのグローバル名前空間を汚さないで済むということですね。

とくにwrap=trueの場合につくられる無名モジュールは、ロード後にGCによって刈られるため、痕跡を残さない、と言うことのようです。

2. selfがmainオブジェクトのクローンと差し変わる

wrapが設定された場合は上にあるように、ロードの処理がload_wrappingに渡されます。

以下のように、ロード直前にselfが差し代わっているのが読み取れます。

    th->top_self = rb_obj_clone(rb_vm_top_self()); /* 右辺は、Rubyでいうところのself.cloneをトップレベルで実行することに相当する */

rb_vm_top_self()というのは、通常のトップレベルのself、つまり我々が main と呼んでいるものです。
このmainをクローンし、それを一時的にselfに設定しています(ロード後はGCに掃除されていなくなる)

wrapオプション設定時、インスタンス変数は、この「クローンされたmain」のインスタンス変数として登録されます。

mainと同じ機能を持つダミーオブジェクト内でロードスクリプトを実行することで、元のトップレベルmainを汚さずに処理を行える、というトリックなんですね。

3. ダミーメインのシングルトンメソッドが定義される

さて、2で生成されたmainクローンは、1で触れたラッパーモジュールでextendされます。

    rb_extend_object(th->top_self, th->top_wrapper); /* self.extend(wrap) に相当する。wrapにモジュールを設定しない場合、生成された無名モジュールがここにはいる */

ロードスクリプトのトップレベルで定義されたメソッドはラッパーモジュールに生えますが、このextendによって同時にクローンされたmainオブジェクトのシングルトンメソッドにも設定されます。

このことにより、ロードスクリプト内で定義したメソッドを同じスクリプト内で呼ぶことができるようになります。
(ロードスクリプトはselfをmainクローンに設定した状態で実行されることを思い出してください。レシーバのないメソッド呼び出しに対しては、selfであるmainクローンが応答する必要がありますね。)

extendのメリットをRubyで雑に表現するとこのような感じでしょうか。

module Wrap
  # ロードされたスクリプト内で定義されたメソッド
  def some_method = puts 'I am here!'
end

cloned = self.clone
cloned.extend Wrap

cloned.instance_eval do
 # ここでロードされたスクリプトの処理が行われる(イメージ)
 some_method # ここの中で呼び出せる
end

# I am here!

処理がmainに帰った後は、クローンにアクセスできなくなるので、メソッドを定義しても元のトップレベルに痕跡を残さない、ということが可能となっています。

Loadはどこに定義を隠すのか(まとめ)

ソースコードレベルで細かく掘ると、ロード時のトップレベル定義の行き先はこんな感じであることがわかりました。

wrapオプション 定数の定義先 メソッド定義 インスタンス変数 ローカル変数
false(デフォルト) Object Objectのprivateインスタンスメソッド main
true 無名モジュール 無名モジュール(mainクローンのシングルトンメソッド) mainクローン
モジュール モジュール モジュール mainクローン

無名モジュールやmainクローンについては、一般的に持ち出せるものではないため、実質上は「呼び出し側に影響を及ぼすことはない」と考えて問題なさそうです。

消されたものたちとのコンタクトをはかる

ソースコードにより、loadによって隠されたものたちがどこにあるのかを把握しました。

せっかくここまできたら、実際に行方不明に見えたものたちを取り出すことで、この話を検証していきましょう。(ええ、意地です)

まずは簡単なものから・・・

ローカル変数を取り出す

マニュアルにも書いてあるようにどうやっても取り出すことはできません。
とはいえ当然ながら、ロード時のbindingを取得することができればなんとかなりそうですね。まあそれはそう。

まずロードされるスクリプトを用意します。中ではローカル変数を利用しています。

# foo.rb

foo = :foo
bar = :bar

$b = binding # bindingをグローバル変数に退避する

次にこれをロードしてみます。なお、コレに関しては第二オプションにかかわらず、同じ挙動になります。

load 'foo.rb', true

eval 'foo', $b # => foo
eval 'bar', $b # => bar

アクセスできました。
・・・まあ束縛を持ち運んでいるわけだし・・それはそう(2回目)

インスタンス変数はどこに

Rubyではインスタンス変数は何らかのオブジェクト(定義時のself)に紐づかなければなりません。
さて、ここでいう「オブジェクト」は何なのでしょう?

すでに書いたように、loadでwrapオプションを設定した場合、mainオブジェクトのクローンが作られ、そこをselfとした環境でスクリプトが実行されます。
ロードされたファイル内で定義されたインスタンス変数は、このクローンオブジェクトに定義されているはずです。

確認のため、ちょっと頑張ってこのクローンを取り出してみましょう。

まずロードされるスクリプトにインスタンス変数を定義します。

# foo.rb

# インスタンス変数を定義する
@foo = :foo

# ついでにメソッドも定義しておく
def bar
  puts :bar
end

例によってこのファイルをロードします。GCは切っておきます。

# mainのクローンオブジェクトが消されないよう、GCを止めておきます
GC.disable

# ロード前に、全モジュール(クラス)を取得しておく
m1 = ObjectSpace.each_object(Module).to_a

# ロードする(モジュール名を指定するオプションでも同じ結果になる)
load 'foo.rb', true

# ロード後の全モジュール(クラス)を取得
m2 = ObjectSpace.each_object(Module).to_a

new_modules = m2 - m1 # ロード前後で増えたモジュール・クラスを取得する
# => [#<Class:#<Object:0x000000010c9f3ac8>>, #<Module:0x000000010c9f3af0>]

ObjectSpaceを見ることで、ロード前後でのモジュール(クラス)の差分が取得できます。

みてみると、#<Class:#<Object:0x000000010c9f3ac8>> という、Objectクラスのインスタンスにおけるシングルトンクラスがいます。
どうやらこれが怪しそうです。

「これをシングルトンクラスとして持つ、元のObjectインスタンス」を取り出してみましょう。
仮にtargetという変数名にしておきます。

target = ObjectSpace.each_object.select { |obj| obj.instance_of? Object and obj.singleton_class == new_modules.first }.first
target.to_s                         # => "main"
target.class                        # => Object

targetはObjectクラスのインスタンスでmainと名乗っているようですが、元々のmain(トップレベルにおけるself)とは別物です。

target == self                      # => false

結論から言えば、これがロード時に生成されたmainのクローンになります。
ロードされたスクリプト内で定義したインスタンス変数は、このオブジェクトの中にいます。

target.instance_variable_get(:@foo) # => :foo

やったぜ・・!つかまえました!(何が嬉しいのかはわからない)

メソッドのゆくえ

上で取得したmainのクローンに対して、メソッドを問い合わせてみましょう。

target.send :bar # => bar

ビンゴですね!
ただすでに述べた通り、このメソッドは直接オブジェクトに生えるわけではなく、ラッパーモジュールに生えています。

wrap=trueオプションを設定した場合、ラッパーモジュールは無名モジュールになるはずでした。
よくよく見るとロード時に生まれたnew_modulesに、それらしきものがいますね。

new_modules
# => [#<Class:#<Object:0x000000010c9f3ac8>>, #<Module:0x000000010c9f3af0>]

取り出してみましょう。

mod = new_modules.last
mod.instance_methods(false)

先程のメソッドはここに定義されています。

# メソッドがmod上に定義されていることを確認(トップレベルは可視性がprivateに設定されている)
> mod.private_instance_methods(false)
=> [:bar]

ソースコードを読んで想像したことが検証できましたね。

もちろん第二引数にモジュールを定義した場合、ロードしたファイル内で定義したメソッドはそのモジュールに生えることになります。

定数の行き先

最後に、定数はどこに行くのでしょうか。
さっそく定数をファイルに定義してロードしてみましょう。

# bar.rb

HOGE = 'hoge' # 定数を定義する

オプションとしてモジュールを入れた場合は、そのモジュールに定義されることが簡単に確認できます。

module M; end
load 'hoge.rb', M

M.constants(false)  # => [:HOGE]

オプションとしてwrap=trueを指定した場合、どこに行くのでしょうか?

GC.disable
m1 = ObjectSpace.each_object(Module).to_a
load 'foo.rb', true
m2 = ObjectSpace.each_object(Module).to_a

new_modules = m2 - m1
# => [#<Class:#<Object:0x000000010b26b220>>, #<Module:0x000000010b26b248>]

例によって、ここで生成された無名モジュールが怪しいですね。

mod = new_modules.last
mod.constants(false)  # => [:HOGE]

全てソースコードから推測したとおりだったことを確認できました。
それぞれ巧妙に隠されていることがよくわかりますね〜。

requireのファイルロックはどうやっているのか?

脱線ついでにおまけの検証です。
loadにはない、「同じファイルを一度だけロードする」というrequireの仕様はどうやって実現されているのでしょうか。

ドキュメントにも触れられている通り、requireのファイルロックは内部で$LOADED_FEATURE ($")というグローバル変数を操作することで実現されています。

# 初期状態を保存しておく
a = $LOADED_FEATURES.dup

# 適当なファイルをloadする
load './test.rb'         #=> true
b = $LOADED_FEATURES.dup
b == a                   # => true ... load後も$LOADED_FEATURESの中身は変わらない

# 同じファイルをrequireしてみる
require './test.rb'      # => true
c = $LOADED_FEATURES.dup
c == a.                  # => false
c - a                    # => ["/Users/xxxxx/yyyyy/test.rb"] ... requireしたファイルが新たに登録されている

# 再度requireすると失敗する
require './test.rb'      # => false

# もちろんloadは$LOADED_FEATURESによらず常に可能
load './test.rb'         #=> true

つまり、requireはまずファイルの絶対パスを`$LOADED_FEATURE'で確認し、もし登録されていなければそのファイルをロードした上で、ファイルパスを新規に追加します。

一方、loadはこの変数を変更・参照することはなさそう、ということもわかりますね。

ではもしこの配列を操作した場合は、多重にrequireできるのでしょうか?

# あらかじめ絶対パスを取得しておく
filepath = File.join(Dir.pwd, 'test.rb')

require filepath                      # => true
require filepath                      # => false # 2度目はNG

# このパスを$LOADED_FEATURESから削除してみる
$LOADED_FEATURES.delete filepath

require filepath                      # => true

はい。
なんと、すでにrequireしたファイルが再度requireできました〜 :clap: :clap: :clap:

いやいやいや、普通そんな凶悪な使い方する奴おらんやろ!!!!

そう思ったそこのあなたは、ぜひzeitwerkを見てビックリしましょう4(ビックリ)

・・・とはいえ普通の開発でこんなハックは不要ですね。
実務的な結論は「requireを普通に使え」というオチでした。

で、loadのユースケースは結局あったんだっけ・・・🤔

参考ドキュメント

  1. 他にautoloadなんかもありますが、今はzeitwerkがやってくれるので意識することはありませんね

  2. ちなみにロードされたファイルから、さらに別のファイルをロードしても、同じスコープを引き継ぎます。
    たとえば、wrap=trueでロードしたファイルがあるとします。そのファイル内でロードされた孫ファイルで定義された定数やメソッドは、wrap=trueとして同じように隠されます。

  3. これ以外のオブジェクトをオプションに渡した場合ちょっと変わった挙動をします。

  4. 開発環境におけるRailsのreloadを実現するためやむを得ずこれをやっているようですね

7
4
4

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
7
4