追記:コメントを受けて一部内容を修正して、後日談を追記しました。
環境
Ruby 2.5.0
背景
こんにちは!エンジニアになって9ヶ月になるまだまだひよっこのエンジニアです。
昼休みに先輩とRubyの定数探索の話をしていたのですが、その時いくつか謎が出てきて、昼休みが終わるまでに解決できなかったので、家に帰って延長戦をすることにしました。
今回はその調査結果をまとめました。
参考書籍:「Rubyのしくみ」
Rubyの定数探索の基礎
まずはRubyの定数探索の基礎から。
Rubyの定数探索は主に二つの観点から行われます。
一つ目がレキシカルスコープで、Rubyはクラスやモジュールをネストすると、Ruby内部のnd_next
ポインタを一つ外側のクラスに設定します。そして、定数探索の時はそのnd_next
ポインタを辿って定数探索の解決をします。
module A
HOGE = 'A'
class B
def hoge
puts HOGE
end
end
end
とこのようにネストされた、class B
で定数HOGE
を参照するようにすると、下記のようにmodule A
の定数が出力されます。
irb(main)> A::B.new.hoge
A
=> nil
もちろん複数回ネストしても、探索されますし、トップレベルも探索対象になります。
(後述するが、トップレベルは正確にはレキシカルスコープの探索とは異なる。)
TOP = 'top'
module A
HOGE = 'A'
module B
FUGA = 'B'
class C
def output
puts TOP
puts HOGE
puts FUGA
end
end
end
end
=> :output
irb(main)> A::B::C.new.output
top
A
B
=> nil
と、このようにレキシカルスコープを辿って探索されます。
二つ目はスーパークラスチェーンです。各スーパークラスのチェーンを辿って定数解決を行います。
class A
HOGE = 'A'
end
class B < A
def hoge
puts HOGE
end
end
irb(main)> B.new.hoge
A
=> nil
このようにスーパークラスを辿って定数探索を行います。
定数探索はこの二つがありますが、優先順位としては、レキシカルスコープが優先され、その後に、スーパークラスが探索されることになります。実際に下記で実験してみると、レキシカルスコープが優先されているのがわかると思います。
class A
HOGE = 'A'
end
module B
HOGE ='B'
class C < A
def hoge
puts HOGE
end
end
end
irb(main)> B::C.new.hoge
B
=> nil
このようにレキシカルスコープが優先されて、スーパークラスの探索が劣後します。
Rubyの定数探索の基礎がわかったところで、謎に挑みたいと思います。
謎その1(トップレベルの探索順位)
一つ目の謎はトップレベルの探索順位です。最初に見たように、定数探索においては、レキシカルスコープが優先されます。それでは下記のように定義するとどうなるでしょうか?
HOGE = 'TOP'
class A
HOGE = 'A'
end
class B < A
def hoge
puts HOGE
end
end
レキシカルスコープから探索されるので、ネストの一番上位である、トップレベルのTOP
が出るのでしょうか?
実際には下記のようになります。
irb(main)> B.new.hoge
A
=> nil
なんと、トップレベルではなく、スーパークラスが優先されます。この答えはるりまに載っていました。
上記の定数探索の順序の部分を読むと下記のようにありました。
トップレベルの定数定義はネストの外側とはみなされません。したがってトップレベルの定数は、継承関係を探索した結果で参照されるので優先順位は低い と言えます。
つまり、トップレベルはレキシカルスコープの探索の対象外ということです。継承関係の探索結果で参照されるので、スーパークラス内で優先順位の高い、class A
に定義された定数が優先されたということです。
謎その2(定数探索の位置)
続いての謎は定数探索の開始位置の問題です。まず定数探索と似たような探索を行うメソッド探索を見ていきます。
メソッド探索はスーパークラスの探索のみを行います。
class A
def hoge
puts 'スーパークラス'
end
end
module B
def hoge
puts 'レキシカルスコープ'
end
class C < A
def fuga
hoge
end
end
end
irb(main):016:0> B::C.new.fuga
スーパークラス
=> nil
とこのようにスーパークラスのみが探索対象となります。
それでは下記のような場合はどうでしょうか?
class A
def hoge
fuga
end
end
class B < A
def fuga
puts 'fuga'
end
end
このようにスーパークラスでサブクラスのメソッドを呼び出すとどうなるかというと…
irb(main)> B.new.hoge
fuga
=> nil
問題なく呼べます。
それでは定数探索はどうでしょうか?
class A
HOGE = 'A'
def hoge
puts HOGE
end
end
class B < A
HOGE = 'B'
end
irb(main)> B.new.hoge
A
=> nil
なんとサブクラスの定数ではなく、スーパークラスの定数が探索されました。しかもスーパークラスの定数を削除すると…
class A
def hoge
puts HOGE
end
end
class B < A
HOGE = 'B'
end
irb(main)> B.new.hoge
NameError (uninitialized constant A::HOGE)
とこのようにNameError
が発生します。どうやら、定数探索の探索位置はメソッド探索の場合とは異なるようです…
どうなっているのか…
さっぱりお手上げです。
で、終わってしまったら、初心者記事丸出しなので、もう少し頑張ることにします。
もう少し頑張る
しばらく調べていると、Railsガイドにヒントとなる記述がありました。
- ネストが存在する場合、この定数はそのネストの要素の中で順に探索される。それらの要素の先祖は探索されない (訳注: 本章で言う先祖 (ancestors) とはクラス、モジュールのスーパークラスとインクルードしているモジュールのことです)。
2.見つからない場合は、crefの先祖チェーン (継承チェーン) を探索する。
1はレキシカルスコープを探索するということで、基礎で見た通りです。しかし2の記述には見たことがない、cref
という謎の記述があります。これは何でしょうか?
上記の記述の一つ上に下記のような記述があります。
ネストが空でなければその最初の要素、空の場合にはObjectを、コードの任意の場所でのcrefというようにしましょう (訳注: crefはRuby内部におけるクラス参照 (class reference) の略であり、Rubyの定数が持つ暗黙のコンテキストです)。
むむ…なんだか日本語なのに全然わからない…
そこでRubyのしくみを読んでいくと、どうやらcref
は構造体で、nd_next
ポインタとnd_class
ポインタを保持しているようです。
nd_next
ポインタは基礎の部分で見た通り、レキシカルスコープで探索に使われるポインタです。一方、nd_class
はメソッドが定義されている場所のポインタのようです。
上記を勘案すると、定数探索のうち、スーパークラスの探索の時は、メソッドの定義場所を基準として、スーパークラスを探索するのではないかという仮説が立ちます。
ということでclass_eval
で無理やりクラスを変えたらいいのではと思い立ちました。やって見たところ…
class A
def hoge
puts HOGE
end
end
class B < A
HOGE = 'B'
end
class A
B.class_eval do
def fuga
puts HOGE
end
end
end
irb(main)> B.new.hoge
NameError (uninitialized constant A::HOGE)
irb(main)> B.new.fuga
NameError (uninitialized constant A::HOGE)
しかし全然ダメ!!全くうまくいかず…
では上記に続けて、トップレベルや適当なmodule
ではどうかとやってみるも…
B.class_eval do
def fuga
puts HOGE
end
end
irb(main)> B.new.fuga
NameError (uninitialized constant HOGE)
module C
B.class_eval do
def fuga
puts HOGE
end
end
end
irb(main)> B.new.fuga
NameError (uninitialized constant C::HOGE)
やはりダメ…もうダメか、と思いましたが、もう少し続けます。
さらに頑張る
上記の実験を見るに、探索する定数は、selfではなく、定数が定義された位置のネストに基づいて決まっているように思います。実際に下記で実験すると、そのようになっています。
module D
module E
B.class_eval do
def fuga
puts self
puts HOGE
end
end
end
end
irb(main)> B.new.fuga
B
NameError (uninitialized constant D::E::HOGE)
やはりそうみたいですね。self
ではなく、ネストの位置によって決まっているようです。
つまり探索位置が変わっているのではなく、探索対象となる定数が異なっているようです。ということは逆に探索対象を明示的に示してやると探索してくれるのではという仮説が立ちます。
実際にやってみた結果が下記です。
class A
def hoge
puts B::HOGE
end
end
class B < A
HOGE = 'B'
end
irb(main)> B.new.hoge
B
=> nil
いけました!!
つまり探索位置の問題ではなく、探索対象の問題だということでした!!
探索位置自体はサブクラスからスタートしているということですね。
あー!スッキリ!!
結論
定数探索は、レキシカルスコープ、スーパークラスの順に探索されるが、トップクラスはレキシカルスコープでは探索されず、スーパークラスで探索される。
定数を相対参照した場合、(呼び出し元のクラスとは関係なく)定義位置のネスティングを暗黙のうちにかぶせられる。
後日談(追記)
コメントを頂いて、自分が何を勘違いしていたのか、よく考えてみました。私が理解していなかったのは、相対参照の仕組みでした。
相対参照の場合、自分はてっきり、selfによって参照する定数が決まると思っていました。
class A
def hoge
puts HOGE
end
end
class B < A
HOGE = 'B'
end
irb(main)> B.new.hoge
つまり上記の場合、Bがselfになりますので、B::HOGE
を探索するのだと勘違いしていました。しかし相対参照で参照した場合、探索対象の定数は、selfではなく定数を呼び出した、メソッド定義位置のネスティングで決まるということがわかりました。
なお定数の定義自体は定義位置のネスティングとは必ずしも一致しません。継承をした場合がそうです。例えば下記のように、親クラスに定数を作ってそれを子クラスが継承したとしましょう。
class A
HOGE = "hoge"
end
class B < A
def hoge
puts A::HOGE
puts B::HOGE
puts HOGE
end
end
この場合B.new.hoge
はどうなるでしょうか?答えは…
pry(main)> B.new.hoge
hoge
hoge
hoge
=> nil
三つとも全て出ます。では定数をみてみましょう。
pry(main)> A.constants
=> [:HOGE]
pry(main)> B.constants
=> [:HOGE]
ネスティングだけで考えると、A::HOGE
しかないはずですが、上記を見る限り、B::HOGE
も定義されていることになります。そのため、定数の定義はネスティングのみでは決まらないということになります。
ちなみにselfによって探索する定数を変えたい場合はどうするのかというと下記のようにすれば、selfに基づいて、探索対象を決めることが可能です。
class A
def hoge
puts self.class::HOGE
end
end
class B < A
HOGE = "B"
end
pry(main)> B.new.hoge
B
=> nil
まだまだ自分は勉強が足りないなと改めて思いました。
今後も色んな実験を繰り返しながら、理解を深めていきたいと思います。