Ruby
RubyDay 15

Rubyの定数が怖いなんて言わせない

Ruby Advent Calendar 2018の 15 日目です!


定数よ。お前はなぜそんなに難しいのか

使いやすいRubyのメソッドやクラスインスタンス変数に比べて、定数は難しいですね。

私自身、半年に一回は泣かされている弱小エンジニアのひとりです。

せっかくのアドベントの機会を借りて、このハマりがちなRuby定数の謎を徹底的に読み解いてみたいと思います12

対象としてはこんな方を想定しています。


  • Rubyにおけるselfの扱い、継承チェーン、メソッド探索は理解している


  • 定数の挙動でハマったことがある


  • この機会にマニアックな理解をしたい


参考文献としてはCRubyのソースコードに加えて、最後に列挙した数々の貴重な資料を頼らせていただきました(ぺこり)。


定数クイズ

さっそくですが、定数に関する簡単な問題です。

Rubyistなら全部答えられます・・よね?

まず、こんな形であちこちにMyConstという定数を定義します。

MyConst = 'TOPLEVEL'

class GrandParent
# 定義しない
end

class Parent < GrandParent
MyConst = 'Parent'
end

module NameSpace
MyConst = 'NameSpace'
end

module MonkeyPatch
MyConst = 'MonkeyPatch'
end

下のようなクラスChildを考えたとき・・

module NameSpace

class Child < Parent
prepend MonkeyPatch

def put_myconst_at_base
puts ::MyConst
end

def put_myconst
puts MyConst
end

def put_myconst_with_child
puts ::NameSpace::Child::MyConst
end

def put_anything(arg)
puts arg
end

def put_const_with_grandparent
puts GrandParent::MyConst
end
end
end

さて、次のメソッドはそれぞれ何を返すでしょう?

※ Module#prependが導入されたRuby 2.0、現時点最新のRuby 2.5.3の両方で確認しています

# 問題1

NameSpace::Child.new.put_myconst_at_base

# 問題2
NameSpace::Child.new.put_myconst

# 問題3
NameSpace::Child.new.put_myconst_with_child

# 問題4
NameSpace::Child.new.put_anything(MyConst)

# 問題5
NameSpace::Child.class_eval { puts MyConst }

これはちょっとトリッキーなおまけです。 (Ruby2.4以前、2.5以降で挙動が変わります)

# 問題6

NameSpace::Child.new.put_const_with_grandparent

先に答えを知りたい方はこちらへ!


さあ迷宮へ

というわけで、楽しい定数の旅です。

ここからの話は混乱要素がたくさんあるので、まずいくつかの前提を説明するところから入りたいと思います。(以下ではRuby2.5以降を対象とします)

何はともあれRTFMということで、Ruby リファレンスマニュアルを読むところから始めましょう。


ディレクトリ構造に似ている定数参照

るりまを読むと、


あるクラスまたはモジュールで定義された定数を外部から参照する ためには::演算子を用います。またObjectクラスで 定義されている定数(トップレベルの定数と言う)を確実に参照する ためには左辺無しの::演算子が使えます。


ということが書かれています。

定数は::で区切られるということは、もし定数の参照をディレクトリ構造に例えるなら、 ::/ として類推できそうです。

そして :: からはじまるものはルートディレクトリ、つまりObjectクラスに属すると考えれば感覚的にわかりますね。

・・となると、 ::Foo::Bar::Baz という定数参照は、パスで言うところの「絶対参照」のようなイメージでしょうか。

以下のケースについて考えてみましょう。

# クイズの設定を流用します。

module NameSpace
class LittleBrother < Parent
end
end

puts ::NameSpace::LittleBrother::MyConst

このとき、 ::NameSpace::LittleBrother::MyConst は何を指すでしょうか?

もちろんLittleBrotherにそんな定数はないので、もしこれが僕たちの想像する「絶対参照」ならエラーになりそうです。

puts ::NameSpace::LittleBrother::MyConst

=> Parent

なんでや・・なんでなんや・・ :open_mouth:


定数参照の基本

定数参照の解決アルゴリズムは、パス構造のそれとは異なります。

/name_space/little_brother/my_const.rbのようなディレクトリ構造の場合、 / で区切られた前後のディレクトリが直接的に結びついているという前提がありました。

結果として複数の / を挟んで隣り合う要素同士も包含関係を持ちます。つまり最終的に探索されたファイルは、探索開始地点の/name_space/ディレクトリに包含されていると期待できます。

一方Rubyは::NameSpace::LittleBrother::MyConstという定数を、それぞれにパーツに分離して計3回の探索をします。

発見されたオブジェクトは、次の探索の開始点として渡されますが、探索はあらためて行われます。

探索により最終的に見つかった定数名は、::Parent::MyConstであり、最初の探索開始地点である::NameSpaceと直接の関係を持ちません。(混乱ポイント)

スクリーンショット 2018-12-15 23.33.35.png

実は「絶対参照」になるのは最初の定数だけであり、全体が絶対参照になるわけではありません。3

後半の定数は上位の定数の探索結果をベースに、あらためて(絶対参照ではない別の手段で)探索されます。

このとき、上位モジュール内だけではなく、その継承元についても探索対象に含まれるのです。

スクリーンショット 2018-12-15 23.58.31.png

なおこれは絶対参照だけの話ではなく、例えばNameSpace::LittleBrother::MyConstという::から始まらない形についても同じことが言えます。この場合、NameSpaceが見つかったからと言って、下位のLittleBrother, MyConstの位置が自動的に決まるわけではなく、あらためて探索が行われるのです。

さて、ここでちらっと説明した「::のつき方で参照方法が変わる」というのがどういうことなのか、少し掘り下げてみます。


ダブルコロンが挙動を変える

RubyのVMコードであるIseqを観察してみると、定数の探索は getconstant という命令が担当しています。

code = <<~RUBY

module M
puts ::Foo
puts Foo
end
RUBY

puts RubyVM::InstructionSequence.compile(code).disasm # VMコードを確認する

# (以下、大幅に省略しています)

# ::Fooの取得
0005 putobject Object # Objectクラスをスタックに積む
0007 getconstant :Foo # これを手がかりに定数名:Fooの対象を取得する

# Fooの取得
0015 putself # selfをスタックに積む
0019 getconstant :Foo # これを手がかりに定数名:Fooの対象を取得する

定数表記のやり方を変えるとgetconstant命令に引数(スタック)を与える直前のコードが、微妙に変化することがわかります。

CRubyにおける compile.cの実装 を見ると、定数名を区切る::の使い方によって、3通りの微妙に異なるコンパイルがされるようです4

深入りする前に、この3種類の参照方法に対して、この記事で使うための仮の名前をつけておきます5


::Foo


  • 何もない::の後に置かれた定数名を、絶対参照 と呼ぶ。

  • Objectクラスを出発点として探索が行われる

  • ::の前にObjectクラスが自動で補完される以外は、下の修飾つき参照とだいたい同じ。


Foo


  • 頭に何もつかないハダカの定数名を、 相対参照 と呼ぶ。

  • 現在のクラス(厳密に言うともう少し補足がある)を暗黙の出発点として、定数を探索する。

  • この形においては唯一レキシカルコンテキストが関係する


Bar::Foo


  • 前半に置かれた定数名Bar::によって修飾されたFooを、修飾つき参照 と呼ぶ。

  • 前半のBarの部分は、Fooとは独立に相対参照として解決される

具体的にどういうふうに探索方法が変わるかについては次のチャプターで解説します。

ここで重要なことは、2点あります。


  • 定数探索の方法に 3つの異なるパターン があること

  • どの参照方法をとるかは、 個々の定数名と::の位置関係だけ で決まる

定数参照の仕組みについてはもう少し先で掘り下げます。

少しだけ我慢して、まずは参照と比べてシンプルな定数定義を考えたいと思います。


定数が定義されるまで


定数はモジュールかクラスに属する

大前提として、クラスのインスタンスレベルでは定数を持つことはできません。

つまりどんな定数Fooも所属するクラス(モジュール)を用いて、 [クラス(モジュール)参照]::Foo という形に書き下せます6

逆にもし、 Thing::Something という定数参照があるとすれば、前半の Thingは、 必ずクラスかモジュール でなければいけない、ということにもなります(そうでなければSomethingが定義されるべきクラスがない)

もちろん定数の所属するクラス・モジュールは、特異クラスでも、BasicObjectのような基底クラスでもかまいません。


定数のsetter

定数を定義する方法は色々あります。

Module#const_setを使うか、[大文字から始まる定数名]=を使う、もしくはclass/moduleキーワードを使う、という感じでしょうか。

class/moduleキーワードには、定数定義に加えて「クラス(モジュール)オブジェクトをつくる」という特別な機能があります)

定数定義をする処理に関してだけ言うと、おおむねどれも似ています。

簡単のために、ここでは = を使って定数がどのように定義されるのかを見てみましょう。

module M

Foo = 123
end


定数定義はどう行われるのか

実はRubyの実行コンテキストには、self以外にもクラスを指し示すcrefというものがついています。

クラス参照というと、 self.class と同じもののように聞こえますが、必ずしも一致するとは限らない7ので、RubyVMはこれらを独立に保持しています。

コード内にclass/moduleキーワードを置いたり、class_evalやinstance_evalといったメソッドのブロックに入る際に、このcrefが更新されてVMのスタックに積まれていきます。

スクリーンショット 2018-12-17 12.39.27.png

「この場所でdefキーワードを書いたら、どのクラス(モジュール)のメソッドになるのか」ということは、このcrefが示しています。

同じように、「ここで定数を定義したとしたらどのクラス(モジュール)の定数になるのか」ということも、crefをたどることで決定することができます8

もちろん自分のクラスの外側にあるcrefも、スタックをたどることでたどれるようになっています。


crefを使った定数定義

まとめると、定数定義はこうなっています。

プログラムの実行中に :MyConst = 'Here I am!' というコードがあったとき、その地点でのcrefが示すクラスにおいて、MyConstが探索されます。

そのクラスに同名の定数が存在していた場合は警告つきで内容を更新します。

そうでなければ新しいエントリーが定義されて文字列オブジェクト "Here I am!" が登録されます。

もしトップレベルと、現在のコードの間にclass/moduleキーワードがなければ、定数はトップレベル定義になります。

この辺りまで理解できれば、問題4、5が理解できますね。


ネストされた定数定義

ここはRailsなどでよくハマるところなのですが、ネストされた定数定義についても軽く見ておきます。

Parent::Child::Count = 5

こういうコードがあった時、実際は前半のParent::Childまでの部分はただのgetterです。

こう書くと、実際の実行環境のcrefが指すクラスに定数を定義するかわりに、Parent::Childで取得されたクラスかモジュールに対して、 Count = 5 という定数定義が行われます。

Parent::Childという定数名がどういう風に探索されるかということは後でやりますが、基本的に定数定義をしているのは最後の項だけ。

最初の2つの定数名は、Countを修飾しているだけです。シンプルですね。

ネストされた定数を見るときは、1カタマリで見るのではなく、個々がバラバラに動くことを頭に入れておくとわかりやすいかもしれませんね。


まとめ


  • RubyVMはselfとcrefというクラス参照を持っている

  • 頭に何もつかない定数をセットした場合、その地点でのcrefがしめすクラスに対して定数が登録される

  • 修飾済み定数を定義した場合、まず修飾部分の定数が示すクラスが取得され、そのクラスに対して定数が定義される


定数参照の謎を解き明かす

定数参照は、Module#constantsやModule#const_get、class/moduleキーワードを使うことでも発生します。ここではシンプルにFoo::Bar::Bazのような定数名による直接的な参照を考えます。


3つの参照方法

先ほど3パターンの参照があるということを書きました。

絶対参照(::Foo)、相対参照(Foo)、修飾つき参照(Foo::BarにおけるBar)になります。

どの形で定数を参照しても、Ruby内部では最終的に1つのC関数が呼び出されます。

大きな挙動の違いは2つあります。

* 出発点:  探索をスタートする場所が違います

* 探索方法: レキシカルスコープを見るかどうかが異なります

厳密に言うと、トップレベルを見るかとか、privateな定数を対象にするか、など細かい違いもあります。


相対参照

まず、普段よく使う相対参照(Foo)を見ていきましょう。

スタート地点となるcrefを、 cbase と呼ぶことにします。このcbaseは、ほぼ「現地点におけるcref」と同じですが、eval系のメソッドによってスタックに積まれたcrefを飛ばす、という違いがあります。

つまり、cbaseは「一番直近にあったclass/moduleキーワード」によって示されるクラス(モジュール)を指し、これが参照において探索開始点になります。

スクリーンショット 2018-12-17 12.39.27.png

この構造においては、eval系のメソッドがないため、各地点でのcbaseはそのままcrefに一致します。


1) レキシカルな探索

まず最初の探索は、cbaseクラスから出発して、トップレベルの直前まで9crefをたどり、それぞれのクラスに定数が定義されているかを見にいきます。

たどる順番は class/moduleキーワードの位置関係のみ により、レキシカルに決まるため、defの内側・外側だとか、class_evalの中だったとしても影響しません。

外側にあるcrefは、自分自身とは無関係なクラスやモジュールであったとしても、関係なく探索されます。

class C

Foo = 'Foo'
module ::M # トップレベルに定義する(Cとは無関係)
puts Foo # ネストの外側にあるFooを探索する
end
end
=> Foo

この場合は、探索開始点cbaseが::Mにセットされ、次にCが探索されます。(ここでFooの定義が見つかる)

なお、対象の場所でModule.nestingを叩くと、レキシカルな検索の対象となるクラスの一覧が出てきます。

class C

Foo = 'Foo'
module ::M
Module.nesting
end
end
=> [M, C] # [C::M, C]ではなく、::Mと::Cがスタックに積まれている

この探索は継承関係に全く影響されません。

またこれはちょっとクセがありますが、cbaseクラスや外側のクラスに対して、prependしたりincludeして定数を加えたとしても、このアプローチでは探索対象になりません10

スクリーンショット 2018-12-16 0.05.34.png

この辺が理解できると、問題2が納得いくのではないでしょうか。(深い・・)


2) 継承チェーンの探索

探索1が失敗に終わると、Rubyは次に動的なアプローチで探索を始めます。

あらためて最初のcbaseから探索を再開し、今度は継承チェーンを祖先方向にたどります。

こちらはメソッド探索に似ていて理解しやすいですね。

cbaseクラスに対してprependしたりincludeしたモジュール内に、探している定数があった場合はここではじめて検知されます。

なおcbaseがモジュールだった場合、一般的にモジュールは祖先を持たないため、継承探索はすぐに終わります。

ただし、 相対参照のケースに限り、トップレベル(Object)とその祖先を探索対象に加えてくれます。

スクリーンショット 2018-12-16 0.11.49.png


まとめ


  • 相対参照は、2つの探索方法(方向)を持っています。


  • 継承してもレキシカルスコープ上の定数のせいで、探索されないケースがあるので混乱しやすい


  • 3つの中でレキシカルスコープの影響を受けるのは相対参照だけです11



修飾つき参照

ここまで来られれば簡単・・(簡単とは)

次に、 XXX::Fooという形式の修飾つき参照です。

だいたいこのような形で行われます。


  • 直前にくっついている定数をcbaseにとる

  • 「継承チェーンのみ」をたどる

  • トップレベル(Objectクラス)とその祖先は探索しない12

  • privateな定数は探索対象にしない(ヒットしたらエラーをはく)

修飾つき参照ということは、直前にクラスかモジュールを指す定数があるはずなので、そのクラス(モジュール)を探索開始点cbaseにとります。

またこれは、メソッドで言うところの「クラス外からの参照」を意味するためか、private定数は参照できません。

(どうしてもやりたければ、XXX.const_get(:PrivateConst)ということが可能)

なお、修飾つきの定数で継承が出てくると関係性が遠くなって思わぬ定数にヒットをすることがあります。

問題2と問題3の違いに、探索手法の違いによるややこしさが反映されています。伝わるでしょうか・・? :innocent:


絶対参照

最後に絶対参照、一番シンプルです。

再度強調しておくと、絶対参照という名前は仮につけたものなので3少しイメージとは異なります。

(「絶対」だからと言っても、Objectに属する定数に限定されるわけではなく、継承チェーンも探索される)

::の後にくるという形式も含めて、だいたい修飾つき参照と同じような形ですが、少し違いがあります。


  • 探索開始点cbaseがObjectクラスにセットされる

  • 修飾つき参照と異なり、Object(トップレベル)とその祖先を探索する

  • それ以外は、修飾つき参照とほぼ同じ(private定数にヒットしたらエラー)

ここまでくると、::Somethingと、Object::Somethingがどう違うのか?ということが気になりませんか?

実は下のコーナーケース程度の微差があります。

MySecretNumber = 12345

Object = Module.new # Objectという定数名を上書きする

こういうイケナイ遊びをしてみたらどうなるでしょう。

puts Object::MySecretNumber # 相対参照

NameError: uninitialized constant Object::MySecretNumber # 取り出せない!!

Object::MySecretNumberと言う形式だと、まず前半のObjectという相対参照を解決しに行きます。

Objectという名前で示される中身を空のモジュールに差し替えているため、後半はそのモジュールをcbaseに使った修飾つき参照になってしまい、元の値が取り出せなくなります。

一方絶対参照だと・・

puts ::MySecretNumber # 絶対参照

12345 # 取り出せる!

絶対参照は、定数名のマッピングとは関係なく、常に実体としてのObjectクラスをcbaseとして定数探索をしてくれます。

絶対参照があることのメリットが少しだけわかりますね。

いや、こんなことやらないけどw


Ruby2.5の進化

ここまで読み切ったあなたは定数を完全に理解した状態になっているのではと思います。お疲れ様でした!

ここで述べたことはおおむねRuby2.0以降で同じことが言えます。

ただ1点だけ、Ruby2.5から導入された仕様があります。

最後に、ハマりがちなこの挙動について理解して終わりたいと思います。


禁止された探索

修飾つき参照のところで、 トップレベル(Objectクラス)とその祖先は探索しない ということを書きました。

(厳密に言うと、Objectによる修飾つき参照の場合を除き、探索しない12

以前は警告を出しつつも、探索してくれていました。

なぜこれが問題だったのかをRuby公式のチケットから読み解いてみましょう。

# すでに2つのクラスがロードされているとする

class Auth
end

class Twitter
end

Ruby 2.4以前だとこうなります。

Twitter::Auth # 名前空間つきの別のクラスのつもりで参照

(irb):8: warning: toplevel constant Auth referenced by Twitter::Auth
=> Auth # 最初に定義したAuthクラスが帰る

Twitter::Authの部分を、先ほどの話を踏まえて考えてみます。

まず先頭のTwitterの部分は相対参照です。

コードはトップレベルに書かれているため、探索開始点となるcbaseはトップレベルを指します。

つまりトップレベルに定義されたTwitterクラスを返します(ここまではどのバージョンも同じ)

さて、次にRubyは後半の::Authの部分を、cbaseをTwitterクラスにセットした上で探索します。

修飾付き参照なので先祖へと探索に行くはず、ですね。

Twitterクラスの先祖には、様々な基底クラスが含まれています。

> Twitter.ancestors

=> [Twitter, Object, Kernel, BasicObject]

ここで問題となるのはObjectです。

Objectは全てのクラスの祖先でもあり、トップレベルに定義した定数の登録先でもある、という性質を持っています。

この修飾付き参照による先祖方向への探索の結果、トップレベルに定義したはずの定数Authがヒットしてしまうのです。Twitter::Authと書いたエンジニアは、当然::AUthとは別のクラスを指しているつもりですよね。

このせいでクラスが存在するにも関わらず(とくに開発環境で)コードが反映されない、という謎エラーがあるというのが割とツラミでした13

いやー・・こわい話ですねえ。(やられた人)

チケットを読んで見るとRails界隈からの強い要望もあったようで、この警告がエラーになったようです。

この挙動はRubyのバージョンを上げることで解決しますが、なるべくならトップレベルに定数をつくらないように(参照もなるべくしないように)しておくのが幸せの道かもしれません。


大事な教訓 トップレベルに気をつけろ

とはいえ、Railsとかやっているとトップレベルであれこれしないといけなかったり・・ね。


まとめ

定数と仲良くするために大切そうな教訓をまとめてみました。


ネームスペースつき定数の参照は、「素の定数」とは挙動が異なる


  • 具体的には、前者はレキシカルスコープを持つ。後者は持たない

  • レキシカルスコープ上の定数は継承関係にある定数より強い


定数はselfに紐づかない。クラス参照に紐づく


  • 定数探索はメソッド探索に似ているがぜんぜん違う

  • defキーワードやブロックにも影響を受けない


定義や相対参照の際に開始点となるクラス参照がある


  • class_evalやinstance_evalでcrefを切り替えられるが、cbaseは影響を受けない

  • cbaseを切り替えるのは、class/moduleキーワードのみ(class << selfを含む)

  • cbaseはModule.nestingで確認できる(Objectの場合nilになる)


Object(トップレベル定数)とその祖先に注意


  • Objectや、それより上位(KernelやBasicObject)に定数を定義すると色々なことがある

  • Ruby2.4以前と2.5で違いがある

  • よいこはいじらない


定数に他の定数を代入しない


  • 循環参照になる可能性があります

  • とくにクラスやモジュールの名前を代入するのはよろしくないので注意


Railsの定数探索アルゴリズムは異なる


  • Rubyによる定数探索が失敗に終わると、Railsの定数探索が発動します

  • Rubyアドベントなので、詳しくは別のエントリで触れます

・・・やっぱり定数怖ええ!! :scream:


最後に

最近、こういうコーナーケースでハマって数時間を溶かしました。

# 粗雑な例です

class Member
# ...メソッドなど
end

Member.class_eval do # オートロード失敗しないように、classを再オープンせずclass_evalを使っていた
RegistrationFee = 10_000
end

この手の定義がいつの間にかトップレベルで衝突していて、二重定義の警告が出ていたという悲しい話です。

ここまで読んだみなさんは、このどこがいけないのかを完全に理解されたのではないでしょうか?

この手の謎挙動と戦っていたエンジニア仲間の探究心にも触発され、よおし、ボクもこの機会に定数の沼に徹底的につかってやるぞぉという気持ちで書きました。

きっかけを与えてくれた仲間のこんな記事こんな記事にはこの場を借りて感謝させてください m(_ _)m

マニアックすぎる雑記事になりましたが、どなたかのお役に立てれば幸いです。(最後まで読む人がいるのだろうか)


参考文献

たくさんの記事を読みましたが、とくに役に立った参考文献を挙げておきます。


  • 言わずと知れたRuby on Rails Guides。Rubyにおける定数の性質にさかのぼって懇切丁寧に解説してくれている。RubyDocがあっさりしている中、細かいところまで定数の仕様を説明してくれている、(おそらく唯一の)公式資料。Ruby公式よりRails公式の方が充実しているのもなんだかな..w


  • 上にあげたRails Guildesの定数解決の章に関する資料読み解きです。めちゃくちゃわかりやすい神資料なので、とくにRailsで困っている人は最初に読むと良いかと思います。たまたまツイッターで目にして、理解の手がかりにさせていただきました。


  • 『初めてのRuby』の著者でもあるコミッタyugui先生による詳細解説。ソースコードリーディングで行き詰まった頃に知って、何度も膝を打ちました。なんでこれを最初に読まなかったんやワシ・・


  • Rubyのソースコードリーディングといえば「Rubyのしくみ」。定数探索やスタックフレームの仕組み、関連するRubyコードがどこでどうやってiseqにコンパイルされるのか、などなど。定数の沼はこの武器なしには渡りきれないです。


  • 資料ではないのですが、やはりコミッタであるささださん、遠藤さんが主催されているCookpad Ruby Hack Challenge。ソースコードのいじり方を教えていただきました。神々から直接アドバイスがもらえる機会なのでマジオススメ。


  • 最後に、Ruby Hacking Guideです。当時とはコードが随分変わっていますが、やっていることの本質を把握するために何度も戻りました(本買ってない・・)



<クイズの答え>

# 問題1

> NameSpace::Child.new.put_myconst_at_base
TOPLEVEL

# 問題2
> NameSpace::Child.new.put_myconst
NameSpace

# 問題3
> NameSpace::Child.new.put_myconst_with_child
MonkeyPatch

# 問題4
> NameSpace::Child.new.put_anything(MyConst)
TOPLEVEL

# 問題5
> NameSpace::Child.class_eval { puts MyConst }
TOPLEVEL

# 問題6
> NameSpace::Child.new.put_const_with_grandparent
# Ruby 2.4以前
warning: toplevel constant MyConst referenced by GrandParent::MyConst
TOPLEVEL

# Ruby 2.5以降
NameError: uninitialized constant GrandParent::MyConst

1問も悩まなかった方は、ぜひコメント欄に鋭いマサカリをお願いいたします m(_ _)m

※ 僕は最初、自信を持って回答できたのが1番だけでした・・


追記

2018/12/16

問題3の記述を間違えていたため修正。タイポを修正、補足追加

2018/12/17

画像にゴミが入っていたため差し替えました。表記揺れ、タイポの修正など

Twitter::Authの説明を修正 「相対参照」 → 「修飾つき参照」

@takaram さん的確な編集リクエストありがとうございました!

2018/12/18

記事内リンク2ヶ所修正。@ryoff さんご指摘ありがとうございました。





  1. なぜRubyがこんなことをしているのか(WHY)については、私の経験・力量の及ぶところではありませんが、どんな挙動になっているのか」(HOW)という表面的なふるまいだけでも、可能な限り掘り下げたいと思っています。 



  2. なお、以下ではrefinementsやautoloadについては触れませんが、Rubyのしくみを読むと大体わかると思います。 



  3. 絶対参照という言葉はいろいろ誤解をはらむので、「暗黙のObject修飾つき参照」とでも呼びたいところです。が、ちょっと長いのと他に適当な言葉が思いつかないので、この記事では引き続き「絶対参照」と呼ぶことにします。 



  4. ささだ大先生のYARV maniacsに謎解きの解説記事がありました。やはり3種類あるようです。 



  5. (Ruby公式の呼び名がわからないのですが、Rails GuideやC++の類推からつけました) 



  6. p Class.new.tap{|c| c.const_set :Foo, 'unreachable'} みたいにして「参照できない使い捨て定数」もつくれるけど、いいですよねw 



  7. eval系のメソッドでは違いが出る。たとえばメタプログラミングRubyにある「5.1.2 カレントクラス」のところで詳しく説明されています。 



  8. メソッドを定義する基準としてのcrefから、eval系でスタックに積まれたcrefを除外しています。つまり、メソッド定義先はclass_evalで変更できるが、定数の定義先は変わりません。定数探索のベースとなるcrefは、Module.nestingで確認することができます(完全に一致する)。 



  9. トップレベル直前のcrefクラスで探索を終わり、トップレベルは対象になりません。もしトップレベルで相対参照の探索を始めた場合、探索1)は行わずに探索2)に入ります。 



  10. prependの実装は、自分の化身クラスをつくり、prependされたモジュールをサンドイッチのように挟み込んでいる。つまり、元のcbaseクラスをCとすると、化身クラスI(C) < prependモジュール M < originクラス C という状態になっている。I(C)のメソッドテーブルは空にされており、全てのメソッドエントリが上位(origin)クラスCにあるため、メソッド探索ではprependモジュールのメソッドが優先される。一方、定数テーブルは手前にあるI(C)に残されている。そのためレキシカルな検索においては自分自身の定数だけが参照され、祖先方向にたどった場合にはじめてprependされたモジュールの定数が探索されるらしい。 



  11. ソースコードを見ると、前半にレキシカルスコープをたどる、後半に自分の継承チェーンをたどるという処理があるのがわかります。 



  12. 厳密に言えば、Object自身で修飾した場合、つまりObject::Fooというように、cbaseをObjectにおいた時だけは祖先を探索する 



  13. Railsは定数探索失敗時に、Rubyが投げるNameErrorを検知することで、ファイルを動的にオートロードをするという魔改造仕様になっています。間違えたクラスをあてがわれてしまった場合、このオートロードを発動することができず、素知らぬ顔で別のクラスが応答することになります。